feat: add image support to blitz mode

This commit is contained in:
memodack 2025-11-18 20:08:39 +02:00
parent 1495f5eef7
commit e87fe3040a
6 changed files with 123 additions and 8 deletions

View file

@ -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");
}
}

View file

@ -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,
});
});
}

View file

@ -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<IPart[]>;
getSelectedParts(): IPart[];
getSelectedParts(): Promise<IPart[]>;
}
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<IPart[]> {
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;
}
}

View file

@ -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();

View file

@ -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<string>;
getConfigDir(): string;
createFolder(path: string): Promise<TFolder>;
getAbstractFileByPath(path: string): TAbstractFile | null;
getResourcePath(file: TFile): string;
}
@singleton()
@ -22,4 +24,12 @@ export class VaultService implements IVaultService {
createFolder(path: string): Promise<TFolder> {
return this.vault.createFolder(path);
}
getAbstractFileByPath(path: string): TAbstractFile | null {
return this.vault.getAbstractFileByPath(path);
}
getResourcePath(file: TFile): string {
return this.vault.getResourcePath(file);
}
}

View file

@ -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;