From e87fe3040a657e8f3281baffdd5f55173e83c2bf Mon Sep 17 00:00:00 2001 From: memodack <81513409+memodack@users.noreply.github.com> Date: Tue, 18 Nov 2025 20:08:39 +0200 Subject: [PATCH] feat: add image support to blitz mode --- src/services/blitz-modal.service.ts | 22 ++++++++ src/services/blitz.service.ts | 4 +- src/services/parts.service.ts | 78 +++++++++++++++++++++++++++-- src/services/ribbon-icon.service.ts | 2 +- src/services/vault.service.ts | 12 ++++- src/styles.css | 13 +++++ 6 files changed, 123 insertions(+), 8 deletions(-) diff --git a/src/services/blitz-modal.service.ts b/src/services/blitz-modal.service.ts index d18d43d..21d1138 100644 --- a/src/services/blitz-modal.service.ts +++ b/src/services/blitz-modal.service.ts @@ -60,6 +60,18 @@ export class BlitzModalService extends Modal implements IBlitzModalService { const { contentEl } = this; contentEl.empty(); + /** + * Image + */ + const imageUrl = blitz?.imageUrl; + + if (imageUrl) { + this.createImageElement(imageUrl); + } + + /** + * Question + */ this.createQuestionElement(blitz.question); if (blitz.question !== blitz.text && blitz.text) { @@ -71,6 +83,9 @@ export class BlitzModalService extends Modal implements IBlitzModalService { let nextButtonEl: HTMLButtonElement | undefined; let correctOptionEl: HTMLButtonElement | undefined; + /** + * Answers + */ const answersButtons: HTMLButtonElement[] = []; const answersElement = contentEl.createEl("div"); @@ -170,4 +185,11 @@ export class BlitzModalService extends Modal implements IBlitzModalService { questionH2Element.setText(text); questionH2Element.addClass("memodack___blitz__text"); } + + private createImageElement(src: string) { + const { contentEl } = this; + const img = contentEl.createEl("img"); + img.setAttr("src", src); + img.addClass("memodack___blitz__image"); + } } diff --git a/src/services/blitz.service.ts b/src/services/blitz.service.ts index 86ade98..dc023e0 100644 --- a/src/services/blitz.service.ts +++ b/src/services/blitz.service.ts @@ -6,7 +6,8 @@ export interface IBlitz { correctAnswerId: number; question: string; answers: string[]; - text: string; + text?: string; + imageUrl?: string; } export interface IBlitzService { @@ -63,6 +64,7 @@ export class BlitzService implements IBlitzService { question: shufflePartItem.value, answers: shuffleAnswers, text: shufflePartItem.text, + imageUrl: shufflePartItem?.imageUrl, }); }); } diff --git a/src/services/parts.service.ts b/src/services/parts.service.ts index 273c55c..5777524 100644 --- a/src/services/parts.service.ts +++ b/src/services/parts.service.ts @@ -1,3 +1,4 @@ +import { TFile } from "obsidian"; import { inject, singleton } from "tsyringe"; import type { IVaultService } from "./vault.service"; import type { IWorkspaceService } from "./workspace.service"; @@ -6,11 +7,17 @@ export interface IPart { value: string; translation: string; text?: string; + imageUrl?: string; } export interface IPartsService { getParts(): Promise; - getSelectedParts(): IPart[]; + getSelectedParts(): Promise; +} + +interface IExternalLink { + title: string; + url: string; } @singleton() @@ -57,23 +64,36 @@ export class PartsService implements IPartsService { rawText = rawText.replace(item[0], item[1]); }); - parts.push({ + const data: IPart = { value: match[1], translation: match[2], text: rawText, - }); + }; + + /** + * Image + */ + const imageUrl = this.findAnImageUrl(content, match[1]); + + if (imageUrl) { + data.imageUrl = imageUrl; + } + + parts.push(data); }); return parts; } - getSelectedParts(): IPart[] { + async getSelectedParts(): Promise { const activeFile = this.workspaceService.getActiveFile(); if (!activeFile) { return []; } + const content = await this.vaultService.read(activeFile); + const parts: IPart[] = []; const selection = window.getSelection(); @@ -99,7 +119,18 @@ export class PartsService implements IPartsService { // Format the string and add it to the array if (value && translation) { - parts.push({ value, translation }); + const data: IPart = { value, translation }; + + /** + * Image + */ + const imageUrl = this.findAnImageUrl(content, value); + + if (imageUrl) { + data.imageUrl = imageUrl; + } + + parts.push(data); } } }); @@ -112,4 +143,41 @@ export class PartsService implements IPartsService { return []; } + + private findAnImageUrl(content: string, search: string): string | null { + const filteredImages = this.extractExternalLinks(content).map((item) => ({ + title: item.title, + url: this.getResourcePath(item.url), + })); + + const image = filteredImages.find((item) => item.title === search); + + if (!image) { + return null; + } + + return image.url; + } + + private extractExternalLinks(content: string): IExternalLink[] { + // [text](url) + const regex = /\[([^\]]+)\]\(([^)]+)\)/g; + + return [...content.matchAll(regex)].map((m) => ({ + title: m[1], + url: m[2], + })); + } + + private getResourcePath(path: string) { + let url = path; + + const file = this.vaultService.getAbstractFileByPath(url); + + if (file instanceof TFile) { + url = this.vaultService.getResourcePath(file); + } + + return url; + } } diff --git a/src/services/ribbon-icon.service.ts b/src/services/ribbon-icon.service.ts index e5146ad..b8e083a 100644 --- a/src/services/ribbon-icon.service.ts +++ b/src/services/ribbon-icon.service.ts @@ -39,7 +39,7 @@ export class RibbonIconService implements IRibbonIconService { let parts: IPart[] = []; - parts = this.partsService.getSelectedParts(); + parts = await this.partsService.getSelectedParts(); if (!parts.length) { parts = await this.partsService.getParts(); diff --git a/src/services/vault.service.ts b/src/services/vault.service.ts index f94395f..54bc1dd 100644 --- a/src/services/vault.service.ts +++ b/src/services/vault.service.ts @@ -1,10 +1,12 @@ -import type { TFile, TFolder, Vault } from "obsidian"; +import type { TAbstractFile, TFile, TFolder, Vault } from "obsidian"; import { inject, singleton } from "tsyringe"; export interface IVaultService { read(file: TFile): Promise; getConfigDir(): string; createFolder(path: string): Promise; + getAbstractFileByPath(path: string): TAbstractFile | null; + getResourcePath(file: TFile): string; } @singleton() @@ -22,4 +24,12 @@ export class VaultService implements IVaultService { createFolder(path: string): Promise { return this.vault.createFolder(path); } + + getAbstractFileByPath(path: string): TAbstractFile | null { + return this.vault.getAbstractFileByPath(path); + } + + getResourcePath(file: TFile): string { + return this.vault.getResourcePath(file); + } } diff --git a/src/styles.css b/src/styles.css index 5a8029c..e2b278e 100644 --- a/src/styles.css +++ b/src/styles.css @@ -86,6 +86,19 @@ margin-top: 0; } +.memodack___blitz__image { + height: 170px; + min-height: 170px; + border-radius: var(--modal-radius); + border: var(--modal-border-width) solid var(--modal-border-color); + box-shadow: var(--input-shadow); + display: block; + margin-left: auto; + margin-right: auto; + object-fit: cover; + aspect-ratio: 4 / 3; +} + @media (prefers-color-scheme: dark) { .memodack___syntax { text-decoration-color: #6f6f6f;