diff --git a/HandTranscriptMd/manifest.json b/HandTranscriptMd/manifest.json index 6164881..abd86ce 100644 --- a/HandTranscriptMd/manifest.json +++ b/HandTranscriptMd/manifest.json @@ -1,6 +1,6 @@ { "id": "handwriting-to-markdown", - "name": "Handwriting to Markdown", + "name": "HandTranscriptMd", "version": "0.1.0", "minAppVersion": "0.15.0", "description": "Inline handwriting canvas with conversion to structured markdown", diff --git a/HandTranscriptMd/src/editor-view.ts b/HandTranscriptMd/src/editor-view.ts index 6ea6314..a4c0c55 100644 --- a/HandTranscriptMd/src/editor-view.ts +++ b/HandTranscriptMd/src/editor-view.ts @@ -5,13 +5,14 @@ handwriting Android. ============================================= */ -import { ItemView, WorkspaceLeaf, TFile, Notice, Platform, Modal, App } from 'obsidian'; +import { ItemView, WorkspaceLeaf, TFile, Notice, Platform, Modal, App, MarkdownView } from 'obsidian'; import type HandwritingPlugin from './main'; import { DrawingCanvas, Stroke } from './drawing-canvas'; import { strokesToSvg, parseSvgStrokes } from './svg-utils'; import { getEffectiveBgColor, getEffectiveLineColor, remapStrokeColor } from './settings'; import { getRecognizer } from './recognizer'; import { parseMarkdown } from './md-parser'; +import { t } from './i18n'; export const VIEW_TYPE_HANDWRITING = 'handwriting-editor'; @@ -31,6 +32,7 @@ const ICONS: Record = { 'file-text': ``, 'save': ``, 'x': ``, + 'file-x': ``, 'chevron-down':``, 'chevron-up': ``, 'arrow-left': ``, @@ -98,34 +100,18 @@ export class DrawingEditorView extends ItemView { const lineColor = getEffectiveLineColor(this.plugin.settings); el.style.backgroundColor = bgColor; - // --- Top bar: ← a sinistra | toolbar a destra --- - const topbar = el.createDiv({ cls: 'hwm_editor-topbar' }); + // --- Top bar: toolbar centrata --- + const topbar = el.createDiv({ cls: 'hwm_editor-topbar hwm_editor-topbar--modal' }); if (isDark) topbar.classList.add('hwm_editor-topbar--dark'); - // Bottone ← (torna al documento) - const backBtn = this.mkBtn(topbar, 'arrow-left', 'Torna al documento'); - backBtn.classList.add('hwm_back-btn'); - - // Toolbar (allineata a destra dentro il topbar) + // Toolbar centrata nel topbar const toolbar = topbar.createDiv({ cls: 'hwm_toolbar hwm_editor-toolbar' }); if (isDark) toolbar.classList.add('hwm_toolbar--dark'); - // Toggle compatta (mobile) - if (isMobile) { - toolbar.classList.add('hwm_toolbar--compact'); - const toggleBtn = this.mkBtn(toolbar, 'chevron-down', 'Mostra tutti'); - toggleBtn.classList.add('hwm_toggle-btn'); - toggleBtn.addEventListener('click', () => { - const compact = toolbar.classList.toggle('hwm_toolbar--compact'); - updateColorSizes(compact); - toggleBtn.innerHTML = ICONS[compact ? 'chevron-down' : 'chevron-up'] ?? ''; - }); - } - // Penna / Gomma - const penBtn = this.mkBtn(toolbar, 'pencil', 'Penna'); + const penBtn = this.mkBtn(toolbar, 'pencil', 'btn_pen'); penBtn.classList.add('hwm_active', 'hwm_pen-btn'); - const eraserBtn = this.mkBtn(toolbar, 'eraser', 'Gomma'); + const eraserBtn = this.mkBtn(toolbar, 'eraser', 'btn_eraser'); eraserBtn.classList.add('hwm_eraser-btn'); toolbar.createDiv({ cls: 'hwm_separator' }); @@ -152,16 +138,6 @@ export class DrawingEditorView extends ItemView { } toolbar.createDiv({ cls: 'hwm_separator' }); - // Helper per dimensioni pallini in toolbar compatta - const updateColorSizes = (compact: boolean) => { - colorBtns.forEach(b => { - const sz = (!compact || b.classList.contains('hwm_active')) ? '22px' : '0'; - b.style.setProperty('min-width', sz, 'important'); - b.style.setProperty('min-height', sz, 'important'); - }); - }; - if (isMobile) updateColorSizes(true); - // Listener bgMode: aggiorna toolbar, pallini colore e sfondo canvas al cambio tema. // Deve stare DOPO la dichiarazione di colorBtns per poterli aggiornare nel closure. const lightColors = ['#000000', '#1e40af', '#dc2626', '#16a34a']; @@ -189,22 +165,30 @@ export class DrawingEditorView extends ItemView { this.plugin.bgModeListeners.add(this.bgModeListener); // Undo / Redo / Clear - const undoBtn = this.mkBtn(toolbar, 'rotate-ccw', 'Annulla'); + const undoBtn = this.mkBtn(toolbar, 'rotate-ccw', 'btn_undo'); undoBtn.classList.add('hwm_undo-btn'); - const redoBtn = this.mkBtn(toolbar, 'rotate-cw', 'Ripristina'); + const redoBtn = this.mkBtn(toolbar, 'rotate-cw', 'btn_redo'); redoBtn.classList.add('hwm_redo-btn'); - const clearBtn = this.mkBtn(toolbar, 'trash', 'Cancella tutto'); + const clearBtn = this.mkBtn(toolbar, 'trash', 'btn_clear'); clearBtn.classList.add('hwm_clear-btn'); toolbar.createDiv({ cls: 'hwm_separator' }); // Converti / Salva / Elimina - const convertBtn = this.mkBtn(toolbar, 'file-text', 'Converti in Markdown'); + const convertBtn = this.mkBtn(toolbar, 'file-text', 'btn_convert'); convertBtn.classList.add('hwm_convert-btn'); - const saveBtn = this.mkBtn(toolbar, 'save', 'Salva'); + const saveBtn = this.mkBtn(toolbar, 'save', 'btn_save'); saveBtn.classList.add('hwm_save-btn'); - const deleteBtn = this.mkBtn(toolbar, 'x', 'Elimina riquadro'); + const deleteBtn = this.mkBtn(toolbar, 'file-x', 'btn_delete'); deleteBtn.classList.add('hwm_delete-btn'); + // Bottone chiudi (X): nel topbar, posizionata a destra via CSS absolute + const closeBtn = this.mkBtn(topbar, 'x', 'btn_close'); + closeBtn.classList.add('hwm_close-btn'); + closeBtn.addEventListener('click', async () => { + await this.saveSvg(); + this.leaf.detach(); + }); + // --- Scroll container --- const scrollWrap = el.createDiv({ cls: 'hwm_editor-scroll' }); const canvasWrap = scrollWrap.createDiv({ cls: 'hwm_canvas-wrap' }); @@ -280,10 +264,6 @@ export class DrawingEditorView extends ItemView { }); // Bottone ← → salva e chiudi la tab - backBtn.addEventListener('click', async () => { - await this.saveSvg(); - this.leaf.detach(); - }); } /* ---------- File I/O ---------- */ @@ -355,7 +335,7 @@ export class DrawingEditorView extends ItemView { /* ---------- Elimina ---------- */ private async doDelete() { - if (!confirm('Eliminare questo riquadro handwriting e il file SVG associato?')) return; + if (!confirm(t('confirm_delete'))) return; if (this.canvas) { this.canvas.destroy(); this.canvas = null; } // Rimuovi code block dal .md await this.removeCodeBlock(); @@ -363,7 +343,7 @@ export class DrawingEditorView extends ItemView { const svgFile = this.app.vault.getAbstractFileByPath(this.svgPath); if (svgFile instanceof TFile) await this.app.vault.delete(svgFile); this.leaf.detach(); - new Notice('Riquadro eliminato'); + new Notice(t('btn_delete')); } /* ---------- Manipolazione file .md ---------- */ @@ -423,8 +403,10 @@ export class DrawingEditorView extends ItemView { /* ---------- Helpers ---------- */ - private mkBtn(parent: HTMLElement, icon: string, title: string): HTMLElement { - const btn = parent.createEl('button', { cls: 'hwm_btn', attr: { title } }); + private mkBtn(parent: HTMLElement, icon: string, key: string): HTMLElement { + const label = t(key as any); + const btn = parent.createEl('button', { cls: 'hwm_btn', attr: { title: label } }); + btn.setAttribute('data-hwm-key', key); btn.innerHTML = ICONS[icon] ?? ''; return btn; } @@ -508,30 +490,17 @@ export class DrawingModal extends Modal { const lineColor = getEffectiveLineColor(this.plugin.settings); el.style.backgroundColor = bgColor; - // Top bar con toolbar (nessun backBtn: il Modal ha già la X nativa). - // La classe --modal centra la toolbar orizzontalmente (nel DrawingEditorView - // c'è il backBtn a sinistra e space-between; qui invece la toolbar è l'unico figlio). + // Top bar con toolbar centrata. La X nativa di Obsidian è nascosta via CSS + // (hwm_modal .modal-close-button); la chiusura avviene dal bottone X in toolbar. 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'); - - if (isMobile) { - toolbar.classList.add('hwm_toolbar--compact'); - const toggleBtn = this.mkBtn(toolbar, 'chevron-down', 'Mostra tutti'); - toggleBtn.classList.add('hwm_toggle-btn'); - toggleBtn.addEventListener('click', () => { - const compact = toolbar.classList.toggle('hwm_toolbar--compact'); - updateColorSizes(compact); - toggleBtn.innerHTML = ICONS[compact ? 'chevron-down' : 'chevron-up'] ?? ''; - }); - } - - const penBtn = this.mkBtn(toolbar, 'pencil', 'Penna'); + const penBtn = this.mkBtn(toolbar, 'pencil', 'btn_pen'); penBtn.classList.add('hwm_active', 'hwm_pen-btn'); - const eraserBtn = this.mkBtn(toolbar, 'eraser', 'Gomma'); + const eraserBtn = this.mkBtn(toolbar, 'eraser', 'btn_eraser'); eraserBtn.classList.add('hwm_eraser-btn'); toolbar.createDiv({ cls: 'hwm_separator' }); @@ -552,15 +521,6 @@ export class DrawingModal extends Modal { } toolbar.createDiv({ cls: 'hwm_separator' }); - const updateColorSizes = (compact: boolean) => { - colorBtns.forEach(b => { - const sz = (!compact || b.classList.contains('hwm_active')) ? '22px' : '0'; - b.style.setProperty('min-width', sz, 'important'); - b.style.setProperty('min-height', sz, 'important'); - }); - }; - if (isMobile) updateColorSizes(true); - // Listener bgMode: aggiorna toolbar, pallini colore e sfondo canvas al cambio tema. const lightColors = ['#000000', '#1e40af', '#dc2626', '#16a34a']; const darkColors = ['#ffffff', '#60a5fa', '#f87171', '#4ade80']; @@ -586,21 +546,26 @@ export class DrawingModal extends Modal { }; this.plugin.bgModeListeners.add(this.bgModeListener); - const undoBtn = this.mkBtn(toolbar, 'rotate-ccw', 'Annulla'); + const undoBtn = this.mkBtn(toolbar, 'rotate-ccw', 'btn_undo'); undoBtn.classList.add('hwm_undo-btn'); - const redoBtn = this.mkBtn(toolbar, 'rotate-cw', 'Ripristina'); + const redoBtn = this.mkBtn(toolbar, 'rotate-cw', 'btn_redo'); redoBtn.classList.add('hwm_redo-btn'); - const clearBtn = this.mkBtn(toolbar, 'trash', 'Cancella tutto'); + const clearBtn = this.mkBtn(toolbar, 'trash', 'btn_clear'); clearBtn.classList.add('hwm_clear-btn'); toolbar.createDiv({ cls: 'hwm_separator' }); - const convertBtn = this.mkBtn(toolbar, 'file-text', 'Converti in Markdown'); + const convertBtn = this.mkBtn(toolbar, 'file-text', 'btn_convert'); convertBtn.classList.add('hwm_convert-btn'); - const saveBtn = this.mkBtn(toolbar, 'save', 'Salva'); + const saveBtn = this.mkBtn(toolbar, 'save', 'btn_save'); saveBtn.classList.add('hwm_save-btn'); - const deleteBtn = this.mkBtn(toolbar, 'x', 'Elimina riquadro'); + const deleteBtn = this.mkBtn(toolbar, 'file-x', 'btn_delete'); deleteBtn.classList.add('hwm_delete-btn'); + // Bottone chiudi (X): nel topbar, posizionata a destra via CSS absolute + const closeBtn = this.mkBtn(topbar, 'x', 'btn_close'); + closeBtn.classList.add('hwm_close-btn'); + closeBtn.addEventListener('click', () => this.close()); + const scrollWrap = el.createDiv({ cls: 'hwm_editor-scroll' }); const canvasWrap = scrollWrap.createDiv({ cls: 'hwm_canvas-wrap' }); @@ -694,14 +659,64 @@ export class DrawingModal extends Modal { } catch (e: unknown) { new Notice('Errore OCR: ' + (e instanceof Error ? e.message : String(e))); } } + // Overlay di conferma inline: nessun Modal annidato → nessun furto di focus + private showDeleteConfirm(): Promise { + 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 (!confirm('Eliminare questo riquadro handwriting e il file SVG associato?')) return; + 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('.cm-content') as HTMLElement; + cm?.focus(); + }, 300); + }; + + // Registra il listener PRIMA di modificare il file, così non perdiamo l'evento. + // vault.on('modify') scatta con certezza quando removeCodeBlock() scrive il file. + const ref = this.app.vault.on('modify', (file) => { + if (file.path === srcPath) { + this.app.vault.offref(ref); + doFocus(); + } + }); + await this.removeCodeBlock(); const svgFile = this.app.vault.getAbstractFileByPath(this.svgPath); if (svgFile instanceof TFile) await this.app.vault.delete(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('Riquadro eliminato'); + new Notice(t('btn_delete')); } private async archiveSvg() { @@ -744,8 +759,10 @@ export class DrawingModal extends Modal { await this.replaceInMd('\n'); } - private mkBtn(parent: HTMLElement, icon: string, title: string): HTMLElement { - const btn = parent.createEl('button', { cls: 'hwm_btn', attr: { title } }); + private mkBtn(parent: HTMLElement, icon: string, key: string): HTMLElement { + const label = t(key as any); + const btn = parent.createEl('button', { cls: 'hwm_btn', attr: { title: label } }); + btn.setAttribute('data-hwm-key', key); btn.innerHTML = ICONS[icon] ?? ''; return btn; } @@ -763,3 +780,4 @@ export class DrawingModal extends Modal { }); } } + diff --git a/HandTranscriptMd/src/embed.ts b/HandTranscriptMd/src/embed.ts index b78f33e..6589eea 100644 --- a/HandTranscriptMd/src/embed.ts +++ b/HandTranscriptMd/src/embed.ts @@ -22,10 +22,12 @@ import { const ICONS: Record = { 'file-text': ``, 'x': ``, + 'file-x': ``, 'chevron-up': ``, 'pencil': ``, }; import type HandwritingPlugin from './main'; +import { t } from './i18n'; import { Stroke } from './drawing-canvas'; import { strokesToSvg, parseSvgStrokes, generateId } from './svg-utils'; import { getEffectiveBgColor, getEffectiveLineColor, remapStrokeColor, BgMode } from './settings'; @@ -115,7 +117,7 @@ function setupMutationObserver(plugin: HandwritingPlugin) { const filename = svgPath.split('/').pop() ?? ''; const embedId = filename.replace('.svg', ''); - if (!embedId.startsWith('hw_')) return; + if (!embedId.startsWith('hw_') && !embedId.startsWith('HTMD_')) return; // Se Obsidian non ha ancora caricato l'immagine (classe image-embed // assente), riprova tra 150 ms — il caricamento è asincrono. @@ -248,13 +250,13 @@ function showLegacyPreview( const btnBar = container.createDiv({ cls: 'hwm_inline-buttons' }); if (isDark) btnBar.classList.add('hwm_inline-buttons--dark'); - const deleteBtn = createBtn(btnBar, 'x', 'Elimina riquadro'); + const deleteBtn = createBtn(btnBar, 'file-x', 'Elimina riquadro'); deleteBtn.classList.add('hwm_delete-btn'); const convertBtn = createBtn(btnBar, 'file-text', 'Converti in Markdown'); convertBtn.classList.add('hwm_convert-btn'); - const collapseBtn = createBtn(btnBar, 'chevron-up', 'Comprimi'); + const collapseBtn = createBtn(btnBar, 'chevron-up', 'btn_collapse'); collapseBtn.classList.add('hwm_collapse-btn'); // --- Preview SVG via CSS background-image (nessun dentro cm-content) --- @@ -282,12 +284,14 @@ function showLegacyPreview( preview.classList.remove('hwm_collapsed'); preview.style.maxHeight = ''; collapseBtn.classList.remove('hwm_rotated'); - collapseBtn.title = 'Comprimi'; + collapseBtn.title = t('btn_collapse'); + collapseBtn.setAttribute('data-hwm-key', 'btn_collapse'); } else { preview.classList.add('hwm_collapsed'); preview.style.maxHeight = collapsedHeight + 'px'; collapseBtn.classList.add('hwm_rotated'); - collapseBtn.title = 'Espandi'; + collapseBtn.title = t('btn_expand'); + collapseBtn.setAttribute('data-hwm-key', 'btn_expand'); } }); @@ -307,7 +311,7 @@ function showLegacyPreview( // Elimina deleteBtn.addEventListener('click', async (e) => { e.stopPropagation(); - if (!confirm('Eliminare questo riquadro handwriting e il file SVG associato?')) return; + if (!await showInlineConfirm(container, t('confirm_delete'))) return; await removeLegacyEmbed(ctx, data, plugin); }); } @@ -622,7 +626,7 @@ function createPortalPanel( // --- Bottone matita --- // Desktop: apre DrawingModal (overlay fullscreen, senza aprire nuova tab) // Mobile: apre DrawingEditorView in una nuova tab - const pencilBtn = createPanelBtn(panel, 'pencil', 'Apri editor disegno'); + const pencilBtn = createPanelBtn(panel, 'pencil', 'btn_open_editor'); pencilBtn.addEventListener('click', async () => { if (Platform.isDesktop) { if (modalOpen) return; @@ -659,21 +663,21 @@ function createPortalPanel( panel.appendChild(sep); // --- Bottone converti in Markdown --- - const convertBtn = createPanelBtn(panel, 'file-text', 'Converti in Markdown'); + const convertBtn = createPanelBtn(panel, 'file-text', 'btn_convert'); // --- Bottone comprimi/espandi --- // Usa height + overflow:hidden sul container (non max-height sull'): // così l'immagine viene ritagliata verticalmente senza che la larghezza cambi. // Il pannello (position:absolute, top:6px) resta dentro l'area visibile // anche da compresso (collapsedHeight è sempre >> 6px + altezza pannello). - const collapseBtn = createPanelBtn(panel, 'chevron-up', 'Comprimi'); + const collapseBtn = createPanelBtn(panel, 'chevron-up', 'btn_collapse'); collapseBtn.classList.add('hwm_collapse-btn'); // --- Bottone elimina --- - const deleteBtn = createPanelBtn(panel, 'x', 'Elimina riquadro'); + const deleteBtn = createPanelBtn(panel, 'file-x', 'btn_delete'); deleteBtn.classList.add('hwm_delete-btn'); deleteBtn.addEventListener('click', async () => { - if (!confirm('Eliminare questo riquadro handwriting e il file SVG associato?')) return; + if (!await showInlineConfirm(container, t('confirm_delete'))) return; await removeWikiEmbed(svgPath, sourcePath, plugin); }); @@ -683,14 +687,16 @@ function createPortalPanel( container.style.height = ''; container.style.overflow = ''; collapseBtn.classList.remove('hwm_rotated'); - collapseBtn.title = 'Comprimi'; + collapseBtn.title = t('btn_collapse'); + collapseBtn.setAttribute('data-hwm-key', 'btn_collapse'); }; const doCollapse = () => { isExpanded = false; container.style.height = collapsedHeight + 'px'; container.style.overflow = 'hidden'; collapseBtn.classList.add('hwm_rotated'); - collapseBtn.title = 'Espandi'; + collapseBtn.title = t('btn_expand'); + collapseBtn.setAttribute('data-hwm-key', 'btn_expand'); }; // Carica SVG e chiama doConvertWiki (che lancia eccezione in caso di errore) const doConvertAction = async () => { @@ -740,11 +746,27 @@ function createPortalPanel( plugin.register(() => plugin.bgModeListeners.delete(onBgMode)); } -// Crea un bottone div nel pannello portale -function createPanelBtn(parent: HTMLElement, icon: string, title: string): HTMLElement { +// Overlay di conferma inline su un elemento position:relative. +// Evita window.confirm() che in Electron ruba il focus dalla finestra. +function showInlineConfirm(anchorEl: HTMLElement, msg: string): Promise { + return new Promise(resolve => { + const overlay = anchorEl.createDiv({ cls: 'hwm_confirm-overlay' }); + overlay.createEl('span', { text: msg, 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(); + }); +} + +// Crea un bottone div nel pannello portale. +// key: chiave i18n — usata sia per il title che per data-hwm-key (aggiornamento live al cambio lingua) +function createPanelBtn(parent: HTMLElement, icon: string, key: string): HTMLElement { const btn = document.createElement('div'); btn.className = 'hwm_btn'; - btn.setAttribute('title', title); + btn.setAttribute('title', t(key as any)); + btn.setAttribute('data-hwm-key', key); btn.setAttribute('role', 'button'); btn.setAttribute('tabindex', '0'); btn.innerHTML = ICONS[icon] ?? ''; @@ -808,8 +830,10 @@ function createLegacyPortalButton( // Usa
invece di