gabriele-cusato_HandTranscr.../HandTranscriptMd/src/editor-view.ts
gabriele-cusato c1dc48ab28 fix: resolve ESLint errors, fix parser empty values and TABLE rows, bump to 1.0.1
- Fix collapse/expand animation: use img.naturalWidth (actual SVG viewBox width)
    instead of settings.canvasWidth, which could differ when the canvas auto-widened
    on wide screens; add WAAPI animation replacing CSS height transition
  - Add resolveJsonModule to tsconfig to fix i18n.ts JSON import typing errors
  - Cast JSON.parse results to explicit types in embed.ts and svg-utils.ts
  - Replace Vault.delete() with FileManager.trashFile() in embed.ts and editor-view.ts
  - Remove unused MarkdownView import from main.ts
  - Remove stale eslint-disable comments on WAAPI style assignments
  - Fix md-parser: preserve empty comma-separated values in LIST, NUMLIST, CHECK
    and TABLE headers (removed .filter(item => item))
  - Fix md-parser TABLE: remove body-row continuation (rows ending with comma were
    merged with the next line, causing multiple rows to collapse into one)
  - Bump version to 1.0.1 in manifest.json, package.json, versions.json
2026-03-28 20:04:42 +01:00

626 lines
25 KiB
TypeScript

/* =============================================
DrawingEditorView — Editor in tab Obsidian
Apre il canvas in una tab dedicata, fuori dal
DOM di CodeMirror → nessun conflitto
handwriting Android.
============================================= */
import { ItemView, WorkspaceLeaf, TFile, Notice, Platform, Modal, App, MarkdownView, setIcon, ViewStateResult } from 'obsidian';
import type HandwritingPlugin from './main';
import { DrawingCanvas, Stroke } from './drawing-canvas';
import { strokesToSvg, parseSvgStrokes, svgToBase64Png, archiveSvgFile } from './svg-utils';
import { getEffectiveBgColor, getEffectiveLineColor, remapStrokeColor, LIGHT_COLORS, DARK_COLORS, resolveIsDark } from './settings';
import { getRecognizer } from './recognizer';
import { parseHandwritingToMarkdown } from './md-parser';
import { t, type I18nKey } from './i18n';
export const VIEW_TYPE_HANDWRITING = 'handwriting-editor';
/* =============================================
Utilità condivise tra DrawingEditorView e DrawingModal
============================================= */
// Regex per trovare ![[svgPath]] nel file .md (nuovo formato wiki)
function wikiEmbedRegex(svgPath: string): RegExp {
const esc = svgPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(`\\n?!\\[\\[${esc}\\]\\]\\n?`);
}
// Regex per trovare il code block legacy con l'id specifico
function codeBlockRegex(embedId: string): RegExp {
const esc = embedId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp('\\n?```handwriting\\n.*?"id"\\s*:\\s*"' + esc + '".*?\\n```\\n?', 's');
}
// Applica una sostituzione sul file .md.
// Prova prima il formato wiki ![[svg]], poi il code block legacy come fallback.
async function replaceInMdFile(
mdPath: string,
svgPath: string,
embedId: string,
replacement: string,
plugin: HandwritingPlugin
): Promise<void> {
const mdFile = plugin.app.vault.getAbstractFileByPath(mdPath);
if (!(mdFile instanceof TFile)) { new Notice(t('error_file_not_found')); return; }
const content = await plugin.app.vault.read(mdFile);
let updated = content.replace(wikiEmbedRegex(svgPath), replacement);
if (updated === content) updated = content.replace(codeBlockRegex(embedId), replacement);
if (updated !== content) await plugin.app.vault.modify(mdFile, updated);
}
// Carica i tratti da un file SVG nel vault. Restituisce anche le dimensioni del viewBox.
async function loadStrokesFromSvg(
svgPath: string,
plugin: HandwritingPlugin
): Promise<{ strokes: Stroke[]; canvasWidth: number | null; canvasHeight: number | null }> {
const file = plugin.app.vault.getAbstractFileByPath(svgPath);
if (file instanceof TFile) {
const content = await plugin.app.vault.read(file);
const m = content.match(/viewBox="0 0 (\d+) (\d+)"/);
return {
strokes: parseSvgStrokes(content),
canvasWidth: m ? parseInt(m[1] ?? '0') : null,
canvasHeight: m ? parseInt(m[2] ?? '0') : null,
};
}
return { strokes: [], canvasWidth: null, canvasHeight: null };
}
// Salva il contenuto SVG del canvas su disco e aggiorna la preview inline.
async function saveSvgToDisk(
canvas: DrawingCanvas,
svgPath: string,
embedId: string,
plugin: HandwritingPlugin
): Promise<void> {
const svg = strokesToSvg(
canvas.getStrokes(), canvas.getWidth(), canvas.getHeight(),
canvas.getBgColor(), canvas.getLineColor()
);
const folder = svgPath.substring(0, svgPath.lastIndexOf('/'));
if (folder && !plugin.app.vault.getAbstractFileByPath(folder)) {
await plugin.app.vault.createFolder(folder);
}
const existing = plugin.app.vault.getAbstractFileByPath(svgPath);
if (existing instanceof TFile) {
await plugin.app.vault.modify(existing, svg);
} else {
await plugin.app.vault.create(svgPath, svg);
}
plugin.refreshPreview(embedId, svg);
}
// Crea un bottone con icona Lucide via setIcon.
// Funzione standalone (non metodo) — usata da entrambe le classi editor.
function mkBtn(parent: HTMLElement, icon: string, key: I18nKey): HTMLElement {
const label = t(key);
const btn = parent.createEl('button', { cls: 'hwm_btn', attr: { title: label } });
btn.setAttribute('data-hwm-key', key);
// setIcon: inserisce l'SVG in modo sicuro (no innerHTML)
setIcon(btn, icon);
return btn;
}
/* =============================================
buildEditorUI — Costruisce la toolbar e il canvas
condivisi tra DrawingEditorView e DrawingModal.
Accetta callback per i comportamenti specifici:
- onClose: cosa fare quando si clicca X
- afterCanvas: setup post-canvas (ResizeObserver su Android,
requestAnimationFrame su Desktop)
Restituisce { canvas, bgModeListener } per consentire
alla classe chiamante di fare cleanup in onClose().
============================================= */
async function buildEditorUI(opts: {
el: HTMLElement;
plugin: HandwritingPlugin;
svgPath: string;
embedId: string;
sourcePath: string;
onClose: () => void | Promise<void>;
afterCanvas: (canvas: DrawingCanvas, scrollWrap: HTMLElement, canvasWidth: number) => void;
doSave: () => Promise<void>;
doConvert: () => Promise<void>;
doDelete: () => Promise<void>;
}): Promise<{ canvas: DrawingCanvas; bgModeListener: (bgMode: string) => void }> {
const { el, plugin } = opts;
const isMobile = Platform.isMobile;
const isDark = resolveIsDark(plugin.settings.bgMode);
const bgColor = getEffectiveBgColor(plugin.settings);
const lineColor = getEffectiveLineColor(plugin.settings);
// Sfondo via CSS var: background-color: var(--hwm-bg) in .hwm_editor-view
el.setCssProps({ '--hwm-bg': bgColor });
// --- Top bar: contiene la toolbar centrata e il bottone X ---
const topbar = el.createDiv({ cls: 'hwm_editor-topbar hwm_editor-topbar--modal' });
if (isDark) topbar.classList.add('hwm_editor-topbar--dark');
const toolbar = topbar.createDiv({ cls: 'hwm_toolbar hwm_editor-toolbar' });
if (isDark) toolbar.classList.add('hwm_toolbar--dark');
// Penna / Gomma
const penBtn = mkBtn(toolbar, 'pencil', 'btn_pen');
penBtn.classList.add('hwm_active', 'hwm_pen-btn');
const eraserBtn = mkBtn(toolbar, 'eraser', 'btn_eraser');
eraserBtn.classList.add('hwm_eraser-btn');
toolbar.createDiv({ cls: 'hwm_separator' });
// Palette colori — valori importati da settings.ts (unica fonte di verità)
const colors = isDark ? [...DARK_COLORS] : [...LIGHT_COLORS];
const colorWrap = toolbar.createDiv({ cls: 'hwm_colors' });
const colorBtns: HTMLElement[] = [];
for (const c of colors) {
const btn = colorWrap.createEl('div', {
cls: 'hwm_color-btn',
attr: { title: c, role: 'button', tabindex: '0' }
});
// Colore via CSS var: background-color: var(--hwm-btn-color) in .hwm_color-btn
// Le dimensioni forzate sono ora nel CSS con !important (no più stili inline)
btn.setCssProps({ '--hwm-btn-color': c });
if (c === colors[0]) btn.classList.add('hwm_active');
colorBtns.push(btn);
}
toolbar.createDiv({ cls: 'hwm_separator' });
// Undo / Redo / Clear
const undoBtn = mkBtn(toolbar, 'rotate-ccw', 'btn_undo');
undoBtn.classList.add('hwm_undo-btn');
const redoBtn = mkBtn(toolbar, 'rotate-cw', 'btn_redo');
redoBtn.classList.add('hwm_redo-btn');
const clearBtn = mkBtn(toolbar, 'trash', 'btn_clear');
clearBtn.classList.add('hwm_clear-btn');
toolbar.createDiv({ cls: 'hwm_separator' });
// Converti / Salva / Elimina
const convertBtn = mkBtn(toolbar, 'file-text', 'btn_convert');
convertBtn.classList.add('hwm_convert-btn');
const saveBtn = mkBtn(toolbar, 'save', 'btn_save');
saveBtn.classList.add('hwm_save-btn');
const deleteBtn = mkBtn(toolbar, 'file-x', 'btn_delete');
deleteBtn.classList.add('hwm_delete-btn');
// Bottone chiudi (X): posizionato a destra via CSS absolute
const closeBtn = mkBtn(topbar, 'x', 'btn_close');
closeBtn.classList.add('hwm_close-btn');
closeBtn.addEventListener('click', () => { void opts.onClose(); });
// --- Scroll container e canvas ---
const scrollWrap = el.createDiv({ cls: 'hwm_editor-scroll' });
const canvasWrap = scrollWrap.createDiv({ cls: 'hwm_canvas-wrap' });
// Carica i tratti dal file SVG
const { strokes, canvasWidth: savedW, canvasHeight: savedH } = await loadStrokesFromSvg(opts.svgPath, plugin);
const { canvasWidth, canvasHeight } = plugin.settings;
// Usa le dimensioni salvate nel viewBox per preservare i tratti di sessioni precedenti più larghe
const w = savedW ?? canvasWidth;
const h = savedH ?? canvasHeight;
const debugFn = plugin.settings.debugMode ? (msg: string) => new Notice(msg, 3000) : null;
const canvas = new DrawingCanvas(canvasWrap, w, h, canvasHeight, isMobile, debugFn);
canvas.setBackground(bgColor, lineColor);
canvas.setColor(colors[0]!);
// Su mobile: dito = scroll manuale del container, penna = disegno
if (isMobile) canvas.allowFingerScroll(scrollWrap);
// Carica i tratti con remapping colori al tema corrente
if (strokes.length > 0) {
const remapped = strokes.map(s => ({
...s, color: remapStrokeColor(s.color, plugin.settings.bgMode)
}));
canvas.loadStrokes(remapped);
}
// Setup specifico della classe chiamante (ResizeObserver su Android, rAF su Desktop)
opts.afterCanvas(canvas, scrollWrap, canvasWidth);
// Resize handle (visibile ma non interattivo)
// NOTA: handle è dichiarato dopo colorBtns ma catturato dal bgModeListener per closure:
// la closure legge il valore corrente di 'handle' quando viene invocata (non quando è definita).
let handle!: HTMLElement;
handle = scrollWrap.createDiv({ cls: 'hwm_resize-handle hwm_resize-handle--disabled' });
handle.createEl('span', { text: '⋯' });
handle.classList.toggle('hwm_resize-handle--dark', isDark);
// Listener bgMode: aggiorna toolbar, pallini colore e sfondo canvas al cambio tema.
// Registrato da buildEditorUI e restituito alla classe per poterlo rimuovere in onClose().
const bgModeListener = (bgMode: string) => {
const dark = resolveIsDark(bgMode);
topbar.classList.toggle('hwm_editor-topbar--dark', dark);
toolbar.classList.toggle('hwm_toolbar--dark', dark);
handle.classList.toggle('hwm_resize-handle--dark', dark);
// Sfondo via CSS var (no stile inline)
el.setCssProps({ '--hwm-bg': getEffectiveBgColor(plugin.settings) });
// Aggiorna i pallini colore palette via CSS var
const newColors = dark ? DARK_COLORS : LIGHT_COLORS;
colorBtns.forEach((btn, i) => {
btn.setCssProps({ '--hwm-btn-color': newColors[i] ?? '' });
btn.setAttribute('title', newColors[i] ?? '');
});
// Aggiorna sfondo e righe nel canvas
canvas.setBackground(
getEffectiveBgColor(plugin.settings),
getEffectiveLineColor(plugin.settings)
);
};
plugin.bgModeListeners.add(bgModeListener);
// Auto-scroll quando il canvas si espande, ma solo se non si sta disegnando.
// Durante il disegno, lo scroll sposterebbe il canvas e le coordinate salterebbero.
canvas.onResize(() => {
if (!canvas.isPointerDown()) scrollWrap.scrollTop = scrollWrap.scrollHeight;
});
// --- Event handlers ---
const cv = canvas;
penBtn.addEventListener('click', () => {
cv.setMode('pen');
penBtn.classList.add('hwm_active');
eraserBtn.classList.remove('hwm_active');
});
eraserBtn.addEventListener('click', () => {
cv.setMode('eraser');
eraserBtn.classList.add('hwm_active');
penBtn.classList.remove('hwm_active');
});
for (let i = 0; i < colorBtns.length; i++) {
colorBtns[i]!.addEventListener('click', () => {
colorBtns.forEach(b => b.classList.remove('hwm_active'));
colorBtns[i]!.classList.add('hwm_active');
cv.setColor(colors[i]!);
});
}
undoBtn.addEventListener('click', () => cv.undo());
redoBtn.addEventListener('click', () => cv.redo());
clearBtn.addEventListener('click', () => cv.clear());
convertBtn.addEventListener('click', () => { void opts.doConvert(); });
saveBtn.addEventListener('click', () => { void opts.doSave().then(() => new Notice(t('notice_saved'))); });
deleteBtn.addEventListener('click', () => { void opts.doDelete(); });
return { canvas, bgModeListener };
}
/* =============================================
DrawingEditorView — Tab dedicata (Android)
============================================= */
export class DrawingEditorView extends ItemView {
plugin: HandwritingPlugin;
private canvas: DrawingCanvas | null = null;
private embedId = '';
private svgPath = '';
private sourcePath = '';
private saveTimer: ReturnType<typeof setTimeout> | null = null;
// Listener per aggiornare la classe dark al cambio bgMode
private bgModeListener: ((bgMode: string) => void) | null = null;
// ResizeObserver per adattare il canvas al layout reale (inclusa rotazione schermo)
private displayRo: ResizeObserver | null = null;
constructor(leaf: WorkspaceLeaf, plugin: HandwritingPlugin) {
super(leaf);
this.plugin = plugin;
}
getViewType() { return VIEW_TYPE_HANDWRITING; }
getDisplayText() { return 'Handwriting editor'; }
getIcon() { return 'pencil'; }
getEmbedId() { return this.embedId; }
async setState(state: unknown, result: ViewStateResult) {
// Cast a un tipo strutturato per accedere ai campi in modo type-safe
const s = state as { id?: string; svg?: string; sourcePath?: string } | null;
if (s?.id) this.embedId = s.id;
if (s?.svg) this.svgPath = s.svg;
if (s?.sourcePath) this.sourcePath = s.sourcePath;
// Costruisci la UI solo quando abbiamo i dati
if (this.embedId && this.svgPath) await this.buildEditor();
await super.setState(state, result);
}
getState() {
return { id: this.embedId, svg: this.svgPath, sourcePath: this.sourcePath };
}
async onOpen() { /* UI costruita in setState */ }
async onClose() {
if (this.canvas) {
await this.saveSvg();
this.canvas.destroy();
this.canvas = null;
}
if (this.saveTimer) clearTimeout(this.saveTimer);
// Deregistra il listener bgMode
if (this.bgModeListener) {
this.plugin.bgModeListeners.delete(this.bgModeListener);
this.bgModeListener = null;
}
// Ferma l'osservatore di resize (orientamento schermo)
this.displayRo?.disconnect();
this.displayRo = null;
}
private async buildEditor() {
const el = this.contentEl;
el.empty();
el.classList.add('hwm_editor-view');
const { canvas, bgModeListener } = await buildEditorUI({
el,
plugin: this.plugin,
svgPath: this.svgPath,
embedId: this.embedId,
sourcePath: this.sourcePath,
// Chiude la tab dopo aver salvato
onClose: async () => { await this.saveSvg(); this.leaf.detach(); },
// Adatta il canvas alla larghezza reale e la mantiene sincronizzata
// ad ogni cambio orientamento (portrait ↔ landscape).
// expandWorld=false: la larghezza logica del mondo resta canvasWidth
// → il viewBox dell'SVG salvato non cresce con la larghezza del tablet,
// evitando che la preview inline si accorci e mostri sfondo nero sotto.
afterCanvas: (cv, scrollWrap) => {
this.displayRo = new ResizeObserver(() => {
const displayW = scrollWrap.clientWidth || el.clientWidth;
if (displayW === 0) return;
cv.setDisplayWidth(displayW, false);
});
this.displayRo.observe(scrollWrap);
this.displayRo.observe(el);
},
doSave: () => this.saveSvg(),
doConvert: () => this.doConvert(),
doDelete: () => this.doDelete(),
});
this.canvas = canvas;
this.bgModeListener = bgModeListener;
// Auto-save debounced (2s dopo l'ultimo cambiamento)
canvas.onChange(() => {
if (this.saveTimer) clearTimeout(this.saveTimer);
this.saveTimer = setTimeout(() => { void this.saveSvg(); }, 2000);
});
}
private async saveSvg() {
if (!this.canvas) return;
await saveSvgToDisk(this.canvas, this.svgPath, this.embedId, this.plugin);
}
private async doConvert() {
if (!this.canvas || this.canvas.getStrokes().length === 0) {
new Notice(t('error_no_strokes')); return;
}
// Overlay a tutto schermo sull'editor: spinner + blocco interazione
const overlay = this.contentEl.createDiv({ cls: 'hwm_confirm-overlay hwm_convert-overlay--editor' });
overlay.createDiv({ cls: 'hwm_spinner' });
try {
const svg = strokesToSvg(this.canvas.getStrokes(), this.canvas.getWidth(),
this.canvas.getHeight(), this.canvas.getBgColor(), this.canvas.getLineColor());
const svgEl = new DOMParser().parseFromString(svg, 'image/svg+xml').documentElement as unknown as SVGElement;
const base64 = await svgToBase64Png(svgEl);
const recognizer = getRecognizer(this.plugin.settings.geminiApiKey, this.plugin.settings.ocrLanguages);
const rawText = await recognizer.recognize(base64);
if (!rawText.trim()) throw new Error(t('error_no_text'));
const markdown = parseHandwritingToMarkdown(rawText);
await archiveSvgFile(this.svgPath, this.plugin);
await replaceInMdFile(this.sourcePath, this.svgPath, this.embedId, '\n' + markdown + '\n', this.plugin);
overlay.remove();
this.canvas.destroy(); this.canvas = null;
this.leaf.detach();
new Notice(t('notice_converted'));
} catch (e: unknown) {
// Errore: sostituisce lo spinner con messaggio + OK
overlay.empty();
const msg = e instanceof Error ? e.message : String(e);
overlay.createEl('p', { text: msg, cls: 'hwm_convert-error-msg' });
const okBtn = overlay.createEl('button', { text: 'OK', cls: 'hwm_convert-ok-btn mod-warning' });
okBtn.addEventListener('click', () => overlay.remove(), { once: true });
}
}
// Overlay di conferma inline (come DrawingModal) — evita window.confirm() che
// non funziona in Electron e ruba il focus dalla finestra principale.
private showDeleteConfirm(): Promise<boolean> {
return new Promise(resolve => {
const overlay = this.contentEl.createDiv({ cls: 'hwm_confirm-overlay' });
overlay.createEl('span', { text: t('confirm_delete'), cls: 'hwm_confirm-msg' });
const okBtn = overlay.createEl('button', { text: t('confirm_ok'), cls: 'mod-warning' });
const cancelBtn = overlay.createEl('button', { text: t('confirm_cancel') });
okBtn.addEventListener('click', () => { overlay.remove(); resolve(true); });
cancelBtn.addEventListener('click', () => { overlay.remove(); resolve(false); });
okBtn.focus();
});
}
private async doDelete() {
if (!await this.showDeleteConfirm()) return;
if (this.canvas) { this.canvas.destroy(); this.canvas = null; }
await replaceInMdFile(this.sourcePath, this.svgPath, this.embedId, '\n', this.plugin);
const svgFile = this.plugin.app.vault.getAbstractFileByPath(this.svgPath);
if (svgFile instanceof TFile) await this.plugin.app.fileManager.trashFile(svgFile);
this.leaf.detach();
new Notice(t('notice_deleted'));
}
}
/* =============================================
DrawingModal — Editor disegno come Modal overlay.
Aperto tramite bottone portale (document.body)
per evitare tap su widget CM6.
============================================= */
export class DrawingModal extends Modal {
private plugin: HandwritingPlugin;
private embedId: string;
private svgPath: string;
private sourcePath: string;
private canvas: DrawingCanvas | null = null;
private saveTimer: ReturnType<typeof setTimeout> | null = null;
// Listener per aggiornare la classe dark al cambio bgMode
private bgModeListener: ((bgMode: string) => void) | null = null;
// Callback invocato alla chiusura del modal (usato per nascondere/mostrare il bottone matita)
onClosed?: () => void;
constructor(app: App, plugin: HandwritingPlugin, embedId: string, svgPath: string, sourcePath: string) {
super(app);
this.plugin = plugin;
this.embedId = embedId;
this.svgPath = svgPath;
this.sourcePath = sourcePath;
this.modalEl.addClass('hwm_modal');
}
async onOpen() {
this.contentEl.addClass('hwm_editor-view');
await this.buildEditor();
}
onClose() {
void (async () => {
if (this.canvas) {
await this.saveSvg();
this.canvas.destroy();
this.canvas = null;
}
if (this.saveTimer) clearTimeout(this.saveTimer);
// Deregistra il listener bgMode
if (this.bgModeListener) {
this.plugin.bgModeListeners.delete(this.bgModeListener);
this.bgModeListener = null;
}
// Notifica il chiamante che il modal è stato chiuso
this.onClosed?.();
})();
}
private async buildEditor() {
const el = this.contentEl;
const { canvas, bgModeListener } = await buildEditorUI({
el,
plugin: this.plugin,
svgPath: this.svgPath,
embedId: this.embedId,
sourcePath: this.sourcePath,
// Chiude il modal (Obsidian gestisce il cleanup via onClose)
onClose: () => this.close(),
// Espande il canvas a tutta la larghezza del modal eliminando le bande laterali.
// requestAnimationFrame garantisce che il layout del modal sia pronto prima di misurarlo.
afterCanvas: (cv, scrollWrap, canvasWidth) => {
requestAnimationFrame(() => {
const displayW = scrollWrap.clientWidth;
if (displayW > canvasWidth) cv.setDisplayWidth(displayW);
});
},
doSave: () => this.saveSvg(),
doConvert: () => this.doConvert(),
doDelete: () => this.doDelete(),
});
this.canvas = canvas;
this.bgModeListener = bgModeListener;
// Auto-save debounced (2s dopo l'ultimo cambiamento)
canvas.onChange(() => {
if (this.saveTimer) clearTimeout(this.saveTimer);
this.saveTimer = setTimeout(() => { void this.saveSvg(); }, 2000);
});
}
private async saveSvg() {
if (!this.canvas) return;
await saveSvgToDisk(this.canvas, this.svgPath, this.embedId, this.plugin);
}
private async doConvert() {
if (!this.canvas || this.canvas.getStrokes().length === 0) { new Notice(t('error_no_strokes')); return; }
// Overlay a tutto schermo sul modal: spinner + blocco interazione
const overlay = this.contentEl.createDiv({ cls: 'hwm_confirm-overlay hwm_convert-overlay--editor' });
overlay.createDiv({ cls: 'hwm_spinner' });
try {
const svg = strokesToSvg(this.canvas.getStrokes(), this.canvas.getWidth(), this.canvas.getHeight(),
this.canvas.getBgColor(), this.canvas.getLineColor());
const svgEl = new DOMParser().parseFromString(svg, 'image/svg+xml').documentElement as unknown as SVGElement;
const base64 = await svgToBase64Png(svgEl);
const recognizer = getRecognizer(this.plugin.settings.geminiApiKey, this.plugin.settings.ocrLanguages);
const rawText = await recognizer.recognize(base64);
if (!rawText.trim()) throw new Error(t('error_no_text'));
const markdown = parseHandwritingToMarkdown(rawText);
await archiveSvgFile(this.svgPath, this.plugin);
await replaceInMdFile(this.sourcePath, this.svgPath, this.embedId, '\n' + markdown + '\n', this.plugin);
overlay.remove();
this.canvas.destroy(); this.canvas = null;
this.close();
new Notice(t('notice_converted'));
} catch (e: unknown) {
// Errore: sostituisce lo spinner con messaggio + OK
overlay.empty();
const msg = e instanceof Error ? e.message : String(e);
overlay.createEl('p', { text: msg, cls: 'hwm_convert-error-msg' });
const okBtn = overlay.createEl('button', { text: 'OK', cls: 'hwm_convert-ok-btn mod-warning' });
okBtn.addEventListener('click', () => overlay.remove(), { once: true });
}
}
// Overlay di conferma inline: nessun Modal annidato → nessun furto di focus
private showDeleteConfirm(): Promise<boolean> {
return new Promise(resolve => {
const overlay = this.contentEl.createDiv({ cls: 'hwm_confirm-overlay' });
overlay.createEl('span', { text: t('confirm_delete'), cls: 'hwm_confirm-msg' });
const okBtn = overlay.createEl('button', { text: t('confirm_ok'), cls: 'mod-warning' });
const cancelBtn = overlay.createEl('button', { text: t('confirm_cancel') });
okBtn.addEventListener('click', () => { overlay.remove(); resolve(true); });
cancelBtn.addEventListener('click', () => { overlay.remove(); resolve(false); });
okBtn.focus();
});
}
private async doDelete() {
if (!await this.showDeleteConfirm()) return;
if (this.canvas) { this.canvas.destroy(); this.canvas = null; }
const srcPath = this.sourcePath;
const ws = this.app.workspace;
let focusDone = false;
// Funzione di focus: aspetta 300ms dopo che vault.modify ha sparato,
// in modo da dare all'editor il tempo di completare il re-render del documento.
const doFocus = () => {
if (focusDone) return;
focusDone = true;
setTimeout(() => {
let mdView = ws.getActiveViewOfType(MarkdownView);
if (!mdView || mdView.file?.path !== srcPath) {
const leaf = ws.getLeavesOfType('markdown')
.find(l => (l.view as MarkdownView).file?.path === srcPath);
if (leaf) ws.setActiveLeaf(leaf, { focus: true });
mdView = ws.getActiveViewOfType(MarkdownView);
}
// Focus diretto sul contenteditable CM6
const cm = mdView?.contentEl.querySelector<HTMLElement>('.cm-content');
cm?.focus();
}, 300);
};
// Registra il listener PRIMA di modificare il file, così non perdiamo l'evento.
const ref = this.app.vault.on('modify', (file) => {
if (file.path === srcPath) {
this.app.vault.offref(ref);
doFocus();
}
});
await replaceInMdFile(srcPath, this.svgPath, this.embedId, '\n', this.plugin);
const svgFile = this.app.vault.getAbstractFileByPath(this.svgPath);
if (svgFile instanceof TFile) await this.app.fileManager.trashFile(svgFile);
// Fallback: se vault.modify non spara entro 3s (caso anomalo), forza comunque il focus
setTimeout(() => { this.app.vault.offref(ref); doFocus(); }, 3000);
this.close();
new Notice(t('notice_deleted'));
}
}