aggiunte lingue, fixati bug, mancherebbe solo il bug su mobile del ridimensionamento e l'aggiunta nelle impostazioni per l'aggiunta delle keyword

This commit is contained in:
gabriele-cusato 2026-03-24 18:28:50 +01:00
parent 4da0de3181
commit 0008e40bd2
18 changed files with 909 additions and 165 deletions

View file

@ -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",

View file

@ -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<string, string> = {
'file-text': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>`,
'save': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/><path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"/><path d="M7 3v4a1 1 0 0 0 1 1h7"/></svg>`,
'x': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>`,
'file-x': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><line x1="9.5" y1="12.5" x2="14.5" y2="17.5"/><line x1="14.5" y1="12.5" x2="9.5" y2="17.5"/></svg>`,
'chevron-down':`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>`,
'chevron-up': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>`,
'arrow-left': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>`,
@ -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<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 (!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 {
});
}
}

View file

@ -22,10 +22,12 @@ import {
const ICONS: Record<string, string> = {
'file-text': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>`,
'x': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>`,
'file-x': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><line x1="9.5" y1="12.5" x2="14.5" y2="17.5"/><line x1="14.5" y1="12.5" x2="9.5" y2="17.5"/></svg>`,
'chevron-up': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>`,
'pencil': `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>`,
};
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 <img> 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'<img>):
// 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<boolean> {
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 <div> invece di <button> per i bottoni dentro cm-content.
// I <button> su Android Mobile possono interferire con l'handwriting.
function createBtn(parent: HTMLElement, icon: string, title: string): HTMLElement {
const btn = parent.createDiv({ cls: 'hwm_btn', attr: { title, role: 'button', tabindex: '0' } });
// key: chiave i18n — usata sia per il title che per data-hwm-key (aggiornamento live al cambio lingua)
function createBtn(parent: HTMLElement, icon: string, key: string): HTMLElement {
const btn = parent.createDiv({ cls: 'hwm_btn', attr: { title: t(key as any), role: 'button', tabindex: '0' } });
btn.setAttribute('data-hwm-key', key);
btn.innerHTML = ICONS[icon] ?? '';
return btn;
}

View file

@ -0,0 +1,66 @@
/* =============================================
i18n Localizzazione del plugin
============================================= */
import it from './locales/it.json';
import en from './locales/en.json';
import de from './locales/de.json';
import fr from './locales/fr.json';
import es from './locales/es.json';
import ru from './locales/ru.json';
import ja from './locales/ja.json';
import zhCn from './locales/zh-cn.json';
import ptBr from './locales/pt-br.json';
import pl from './locales/pl.json';
// Mappa codice lingua → dizionario traduzioni.
// I codici corrispondono ai valori restituiti da moment.locale() in Obsidian.
const locales: Record<string, typeof en> = {
it, en, de, fr, es, ru, ja, pl,
'zh-cn': zhCn,
'pt-br': ptBr,
};
// Nome nativo di ogni lingua — usato nel dropdown impostazioni
export const localeNames: Record<string, string> = {
it: 'Italiano',
en: 'English',
de: 'Deutsch',
fr: 'Français',
es: 'Español',
ru: 'Русский',
ja: '日本語',
'zh-cn': '中文(简体)',
'pt-br': 'Português (Brasil)',
pl: 'Polski',
};
// Dizionario attivo — aggiornato da setLocale()
let dict: typeof en = en;
// Imposta la lingua attiva.
// 'auto' rileva la lingua di Obsidian via moment.locale() con fallback a 'en'.
// Dopo aver aggiornato il dizionario, aggiorna tutti i tooltip live nel DOM
// che hanno l'attributo data-hwm-key (impostato da mkBtn/createBtn/createPanelBtn).
export function setLocale(lang: string): void {
const resolved = lang === 'auto'
? ((window as any).moment?.locale?.() ?? 'en')
: lang;
dict = locales[resolved] ?? en;
// Aggiorna i tooltip già presenti nel DOM
document.querySelectorAll<HTMLElement>('[data-hwm-key]').forEach(el => {
const key = el.getAttribute('data-hwm-key') as keyof typeof en;
if (key) el.title = t(key);
});
}
// Funzione di traduzione: ritorna la stringa per la chiave data.
// Se la chiave non esiste nel dizionario attivo usa il fallback inglese.
export function t(key: keyof typeof en): string {
return (dict as Record<string, string>)[key] ?? (en as Record<string, string>)[key] ?? key;
}
// Ritorna i codici lingua disponibili (escluso 'auto')
export function availableLocales(): string[] {
return Object.keys(locales);
}

View file

@ -0,0 +1,50 @@
{
"kw_title": "Titel",
"kw_text": "Text",
"kw_footnote": "Fußnote",
"kw_word": "Wort",
"ui_language_name": "Oberflächensprache",
"ui_language_desc": "Sprache der Einstellungsseite. \"Automatisch\" folgt der Obsidian-Systemsprache.",
"ui_language_auto": "Automatisch (Systemsprache)",
"svg_folder_name": "SVG-Ordner",
"svg_folder_desc": "Ordner im Vault, in dem SVG-Zeichnungsdateien gespeichert werden",
"canvas_width_name": "Canvas-Breite",
"canvas_width_desc": "Horizontale Canvas-Auflösung in Pixeln",
"canvas_height_name": "Canvas-Höhe",
"canvas_height_desc": "Vertikale Canvas-Auflösung in Pixeln",
"bg_mode_name": "Canvas-Hintergrund",
"bg_mode_desc": "Hintergrundfarbe des Zeichenbereichs. \"Automatisch\" folgt dem Obsidian-Theme.",
"bg_mode_auto": "Automatisch (folgt Obsidian-Theme)",
"bg_mode_light": "Hell (weiß)",
"bg_mode_dark": "Dunkel (dunkelgrau)",
"gemini_key_name": "Gemini API-Schlüssel",
"gemini_key_desc": "Erforderlich für die OCR-Handschrifterkennung. Erhalten Sie ihn von Google AI Studio (aistudio.google.com).",
"ocr_langs_name": "OCR-Sprachen",
"ocr_langs_desc": "Kommagetrennte BCP-47-Sprachcodes (z. B. \"it, en, fr\"). Wird vom KI-Modell für OCR verwendet.",
"debug_mode_name": "Debug-Modus",
"debug_mode_desc": "Echtzeit-Benachrichtigungen für IME/Touch-Ereignisse anzeigen (nützlich zur Diagnose von Android-Problemen).",
"keywords_summary": "Vom OCR-Parser erkannte Schlüsselwörter",
"keywords_desc": "Schreiben Sie diese Schlüsselwörter in Ihre Zeichnung, um die entsprechende Markdown-Struktur zu erzeugen. Alle sind Groß-/Kleinschreibung-unabhängig (//h1 = //H1). Der Inhalt folgt nach dem Schlüsselwort, getrennt durch ein Leerzeichen.",
"keywords_col_keyword": "Schlüsselwort",
"keywords_col_syntax": "Syntax",
"keywords_col_output": "Ausgabe",
"menu_expand_all": "Alle Zeichnungen aufklappen",
"menu_collapse_all": "Alle Zeichnungen einklappen",
"menu_convert_all": "Alle Zeichnungen in Text konvertieren",
"error_conversion": "Konvertierungsfehler: ",
"btn_pen": "Stift",
"btn_eraser": "Radierer",
"btn_undo": "Rückgängig",
"btn_redo": "Wiederholen",
"btn_clear": "Alles löschen",
"btn_convert": "In Markdown konvertieren",
"btn_save": "Speichern",
"btn_delete": "Block löschen",
"btn_close": "Schließen",
"btn_collapse": "Einklappen",
"btn_expand": "Aufklappen",
"btn_open_editor": "Zeichnungseditor öffnen",
"confirm_delete": "Diesen Handschrift-Block und die zugehörige SVG-Datei löschen?",
"confirm_ok": "Löschen",
"confirm_cancel": "Abbrechen"
}

View file

@ -0,0 +1,50 @@
{
"svg_folder_name": "SVG folder",
"svg_folder_desc": "Vault folder where drawing SVG files are saved",
"canvas_width_name": "Canvas width",
"canvas_width_desc": "Horizontal canvas resolution in pixels",
"canvas_height_name": "Canvas height",
"canvas_height_desc": "Vertical canvas resolution in pixels",
"bg_mode_name": "Canvas background",
"bg_mode_desc": "Background color of the drawing area. \"Automatic\" follows the Obsidian theme.",
"bg_mode_auto": "Automatic (follows Obsidian theme)",
"bg_mode_light": "Light (white)",
"bg_mode_dark": "Dark (dark gray)",
"gemini_key_name": "Gemini API key",
"gemini_key_desc": "Required for handwriting OCR recognition. Get it from Google AI Studio (aistudio.google.com).",
"ocr_langs_name": "OCR languages",
"ocr_langs_desc": "Comma-separated BCP-47 language codes (e.g. \"it, en, fr\"). Used by the AI model for OCR.",
"debug_mode_name": "Debug mode",
"debug_mode_desc": "Show real-time notifications for IME/touch events (useful for diagnosing issues on Android).",
"keywords_summary": "Keywords recognized by the OCR parser",
"keywords_desc": "Write these keywords in your drawing to generate the corresponding markdown structure. All are case-insensitive (//h1 = //H1). Content follows the keyword separated by a space.",
"keywords_col_keyword": "Keyword",
"keywords_col_syntax": "Syntax",
"keywords_col_output": "Output",
"kw_title": "Title",
"kw_text": "text",
"kw_footnote": "footnote",
"kw_word": "word",
"ui_language_name": "Interface language",
"ui_language_desc": "Language used in the settings page. \"Automatic\" follows the Obsidian system language.",
"ui_language_auto": "Automatic (system language)",
"menu_expand_all": "Expand all drawings",
"menu_collapse_all": "Collapse all drawings",
"menu_convert_all": "Convert all drawings to text",
"error_conversion": "Conversion error: ",
"btn_pen": "Pen",
"btn_eraser": "Eraser",
"btn_undo": "Undo",
"btn_redo": "Redo",
"btn_clear": "Clear all",
"btn_convert": "Convert to Markdown",
"btn_save": "Save",
"btn_delete": "Delete block",
"btn_close": "Close",
"btn_collapse": "Collapse",
"btn_expand": "Expand",
"btn_open_editor": "Open drawing editor",
"confirm_delete": "Delete this handwriting block and the associated SVG file?",
"confirm_ok": "Delete",
"confirm_cancel": "Cancel"
}

View file

@ -0,0 +1,50 @@
{
"kw_title": "Título",
"kw_text": "texto",
"kw_footnote": "nota al pie",
"kw_word": "palabra",
"ui_language_name": "Idioma de la interfaz",
"ui_language_desc": "Idioma usado en la página de ajustes. \"Automático\" sigue el idioma del sistema de Obsidian.",
"ui_language_auto": "Automático (idioma del sistema)",
"svg_folder_name": "Carpeta SVG",
"svg_folder_desc": "Carpeta en el vault donde se guardan los archivos SVG de los dibujos",
"canvas_width_name": "Ancho del canvas",
"canvas_width_desc": "Resolución horizontal del canvas en píxeles",
"canvas_height_name": "Alto del canvas",
"canvas_height_desc": "Resolución vertical del canvas en píxeles",
"bg_mode_name": "Fondo del canvas",
"bg_mode_desc": "Color de fondo del área de dibujo. \"Automático\" sigue el tema de Obsidian.",
"bg_mode_auto": "Automático (sigue el tema de Obsidian)",
"bg_mode_light": "Claro (blanco)",
"bg_mode_dark": "Oscuro (gris oscuro)",
"gemini_key_name": "Clave API de Gemini",
"gemini_key_desc": "Necesaria para el reconocimiento OCR de escritura a mano. Obténgala en Google AI Studio (aistudio.google.com).",
"ocr_langs_name": "Idiomas OCR",
"ocr_langs_desc": "Códigos de idioma BCP-47 separados por comas (p. ej. \"it, en, fr\"). Usados por el modelo de IA para el OCR.",
"debug_mode_name": "Modo depuración",
"debug_mode_desc": "Muestra notificaciones en tiempo real para eventos IME/táctil (útil para diagnosticar problemas en Android).",
"keywords_summary": "Palabras clave reconocidas por el parser OCR",
"keywords_desc": "Escribe estas palabras clave en el dibujo para generar la estructura markdown correspondiente. Todas son insensibles a mayúsculas/minúsculas (//h1 = //H1). El contenido sigue a la palabra clave separado por un espacio.",
"keywords_col_keyword": "Palabra clave",
"keywords_col_syntax": "Sintaxis",
"keywords_col_output": "Salida",
"menu_expand_all": "Expandir todos los dibujos",
"menu_collapse_all": "Contraer todos los dibujos",
"menu_convert_all": "Convertir todos los dibujos a texto",
"error_conversion": "Error de conversión: ",
"btn_pen": "Lápiz",
"btn_eraser": "Borrador",
"btn_undo": "Deshacer",
"btn_redo": "Rehacer",
"btn_clear": "Borrar todo",
"btn_convert": "Convertir a Markdown",
"btn_save": "Guardar",
"btn_delete": "Eliminar bloque",
"btn_close": "Cerrar",
"btn_collapse": "Contraer",
"btn_expand": "Expandir",
"btn_open_editor": "Abrir editor de dibujo",
"confirm_delete": "¿Eliminar este bloque de escritura a mano y el archivo SVG asociado?",
"confirm_ok": "Eliminar",
"confirm_cancel": "Cancelar"
}

View file

@ -0,0 +1,50 @@
{
"kw_title": "Titre",
"kw_text": "texte",
"kw_footnote": "note de bas de page",
"kw_word": "mot",
"ui_language_name": "Langue de l'interface",
"ui_language_desc": "Langue utilisée dans la page des paramètres. \"Automatique\" suit la langue système d'Obsidian.",
"ui_language_auto": "Automatique (langue du système)",
"svg_folder_name": "Dossier SVG",
"svg_folder_desc": "Dossier dans le vault où les fichiers SVG des dessins sont enregistrés",
"canvas_width_name": "Largeur du canvas",
"canvas_width_desc": "Résolution horizontale du canvas en pixels",
"canvas_height_name": "Hauteur du canvas",
"canvas_height_desc": "Résolution verticale du canvas en pixels",
"bg_mode_name": "Fond du canvas",
"bg_mode_desc": "Couleur de fond de la zone de dessin. \"Automatique\" suit le thème d'Obsidian.",
"bg_mode_auto": "Automatique (suit le thème Obsidian)",
"bg_mode_light": "Clair (blanc)",
"bg_mode_dark": "Sombre (gris foncé)",
"gemini_key_name": "Clé API Gemini",
"gemini_key_desc": "Nécessaire pour la reconnaissance OCR de l'écriture manuscrite. Obtenez-la sur Google AI Studio (aistudio.google.com).",
"ocr_langs_name": "Langues OCR",
"ocr_langs_desc": "Codes de langue BCP-47 séparés par des virgules (ex. : \"it, en, fr\"). Utilisés par le modèle IA pour l'OCR.",
"debug_mode_name": "Mode débogage",
"debug_mode_desc": "Affiche des notifications en temps réel pour les événements IME/tactile (utile pour diagnostiquer les problèmes sur Android).",
"keywords_summary": "Mots-clés reconnus par le parser OCR",
"keywords_desc": "Écrivez ces mots-clés dans votre dessin pour générer la structure markdown correspondante. Tous sont insensibles à la casse (//h1 = //H1). Le contenu suit le mot-clé séparé par un espace.",
"keywords_col_keyword": "Mot-clé",
"keywords_col_syntax": "Syntaxe",
"keywords_col_output": "Sortie",
"menu_expand_all": "Développer tous les dessins",
"menu_collapse_all": "Réduire tous les dessins",
"menu_convert_all": "Convertir tous les dessins en texte",
"error_conversion": "Erreur de conversion : ",
"btn_pen": "Stylo",
"btn_eraser": "Gomme",
"btn_undo": "Annuler",
"btn_redo": "Rétablir",
"btn_clear": "Tout effacer",
"btn_convert": "Convertir en Markdown",
"btn_save": "Enregistrer",
"btn_delete": "Supprimer le bloc",
"btn_close": "Fermer",
"btn_collapse": "Réduire",
"btn_expand": "Développer",
"btn_open_editor": "Ouvrir l'éditeur de dessin",
"confirm_delete": "Supprimer ce bloc manuscrit et le fichier SVG associé ?",
"confirm_ok": "Supprimer",
"confirm_cancel": "Annuler"
}

View file

@ -0,0 +1,50 @@
{
"svg_folder_name": "Cartella SVG",
"svg_folder_desc": "Cartella nel vault dove vengono salvati i file SVG dei disegni",
"canvas_width_name": "Larghezza canvas",
"canvas_width_desc": "Risoluzione orizzontale del canvas in pixel",
"canvas_height_name": "Altezza canvas",
"canvas_height_desc": "Risoluzione verticale del canvas in pixel",
"bg_mode_name": "Sfondo canvas",
"bg_mode_desc": "Colore di sfondo del riquadro di disegno. \"Automatico\" segue il tema di Obsidian.",
"bg_mode_auto": "Automatico (segue tema Obsidian)",
"bg_mode_light": "Chiaro (bianco)",
"bg_mode_dark": "Scuro (grigio scuro)",
"gemini_key_name": "Chiave API Gemini",
"gemini_key_desc": "Necessaria per il riconoscimento OCR della scrittura a mano. Ottienila da Google AI Studio (aistudio.google.com).",
"ocr_langs_name": "Lingue OCR",
"ocr_langs_desc": "Codici lingua BCP-47 separati da virgola (es. \"it, en, fr\"). Usati dal modello AI per utilizzare l'OCR.",
"debug_mode_name": "Modalità debug",
"debug_mode_desc": "Mostra notifiche in tempo reale per eventi IME/touch (utile per diagnosticare problemi su Android).",
"keywords_summary": "Keyword riconosciute dal parser OCR",
"keywords_desc": "Scrivi queste keyword nel disegno per generare la struttura markdown corrispondente. Tutte sono case-insensitive (//h1 = //H1). Il contenuto segue dopo la keyword separato da uno spazio.",
"keywords_col_keyword": "Keyword",
"keywords_col_syntax": "Sintassi",
"keywords_col_output": "Output",
"kw_title": "Titolo",
"kw_text": "testo",
"kw_footnote": "nota a piè pagina",
"kw_word": "parola",
"ui_language_name": "Lingua interfaccia",
"ui_language_desc": "Lingua usata nella pagina impostazioni. \"Automatico\" segue la lingua di sistema di Obsidian.",
"ui_language_auto": "Automatico (lingua di sistema)",
"menu_expand_all": "Espandi tutti i disegni",
"menu_collapse_all": "Collassa tutti i disegni",
"menu_convert_all": "Converti tutti i disegni in testo",
"error_conversion": "Errore nella conversione: ",
"btn_pen": "Penna",
"btn_eraser": "Gomma",
"btn_undo": "Annulla",
"btn_redo": "Ripristina",
"btn_clear": "Cancella tutto",
"btn_convert": "Converti in Markdown",
"btn_save": "Salva",
"btn_delete": "Elimina riquadro",
"btn_close": "Chiudi",
"btn_collapse": "Comprimi",
"btn_expand": "Espandi",
"btn_open_editor": "Apri editor disegno",
"confirm_delete": "Eliminare questo riquadro handwriting e il file SVG associato?",
"confirm_ok": "Elimina",
"confirm_cancel": "Annulla"
}

View file

@ -0,0 +1,50 @@
{
"kw_title": "タイトル",
"kw_text": "テキスト",
"kw_footnote": "脚注",
"kw_word": "単語",
"ui_language_name": "インターフェース言語",
"ui_language_desc": "設定ページで使用する言語。「自動」はObsidianのシステム言語に従います。",
"ui_language_auto": "自動(システム言語)",
"svg_folder_name": "SVGフォルダ",
"svg_folder_desc": "描画SVGファイルを保存するvaultのフォルダ",
"canvas_width_name": "キャンバスの幅",
"canvas_width_desc": "キャンバスの水平解像度(ピクセル)",
"canvas_height_name": "キャンバスの高さ",
"canvas_height_desc": "キャンバスの垂直解像度(ピクセル)",
"bg_mode_name": "キャンバスの背景",
"bg_mode_desc": "描画エリアの背景色。「自動」はObsidianのテーマに従います。",
"bg_mode_auto": "自動Obsidianテーマに従う",
"bg_mode_light": "ライト(白)",
"bg_mode_dark": "ダーク(濃いグレー)",
"gemini_key_name": "Gemini APIキー",
"gemini_key_desc": "手書きOCR認識に必要です。Google AI Studioaistudio.google.comから取得してください。",
"ocr_langs_name": "OCR言語",
"ocr_langs_desc": "カンマ区切りのBCP-47言語コード「it, en, fr」。AIモデルのOCRに使用されます。",
"debug_mode_name": "デバッグモード",
"debug_mode_desc": "IME/タッチイベントのリアルタイム通知を表示しますAndroidの問題診断に役立ちます。",
"keywords_summary": "OCRパーサーが認識するキーワード",
"keywords_desc": "これらのキーワードを描画に書くと、対応するmarkdown構造が生成されます。すべて大文字小文字を区別しません//h1 = //H1。内容はキーワードの後にスペースで区切って続けます。",
"keywords_col_keyword": "キーワード",
"keywords_col_syntax": "構文",
"keywords_col_output": "出力",
"menu_expand_all": "すべての描画を展開",
"menu_collapse_all": "すべての描画を折りたたむ",
"menu_convert_all": "すべての描画をテキストに変換",
"error_conversion": "変換エラー:",
"btn_pen": "ペン",
"btn_eraser": "消しゴム",
"btn_undo": "元に戻す",
"btn_redo": "やり直す",
"btn_clear": "すべて消去",
"btn_convert": "Markdownに変換",
"btn_save": "保存",
"btn_delete": "ブロックを削除",
"btn_close": "閉じる",
"btn_collapse": "折りたたむ",
"btn_expand": "展開する",
"btn_open_editor": "描画エディタを開く",
"confirm_delete": "この手書きブロックと関連するSVGファイルを削除しますか",
"confirm_ok": "削除",
"confirm_cancel": "キャンセル"
}

View file

@ -0,0 +1,50 @@
{
"kw_title": "Tytuł",
"kw_text": "tekst",
"kw_footnote": "przypis",
"kw_word": "słowo",
"ui_language_name": "Język interfejsu",
"ui_language_desc": "Język używany na stronie ustawień. \"Automatyczny\" podąża za językiem systemu Obsidian.",
"ui_language_auto": "Automatyczny (język systemu)",
"svg_folder_name": "Folder SVG",
"svg_folder_desc": "Folder w vaulcie, gdzie zapisywane są pliki SVG rysunków",
"canvas_width_name": "Szerokość płótna",
"canvas_width_desc": "Pozioma rozdzielczość płótna w pikselach",
"canvas_height_name": "Wysokość płótna",
"canvas_height_desc": "Pionowa rozdzielczość płótna w pikselach",
"bg_mode_name": "Tło płótna",
"bg_mode_desc": "Kolor tła obszaru rysowania. \"Automatyczny\" podąża za motywem Obsidian.",
"bg_mode_auto": "Automatyczny (podąża za motywem Obsidian)",
"bg_mode_light": "Jasny (biały)",
"bg_mode_dark": "Ciemny (ciemnoszary)",
"gemini_key_name": "Klucz API Gemini",
"gemini_key_desc": "Wymagany do rozpoznawania OCR pisma odręcznego. Uzyskaj go w Google AI Studio (aistudio.google.com).",
"ocr_langs_name": "Języki OCR",
"ocr_langs_desc": "Kody języków BCP-47 oddzielone przecinkami (np. \"it, en, fr\"). Używane przez model AI do OCR.",
"debug_mode_name": "Tryb debugowania",
"debug_mode_desc": "Pokazuje powiadomienia w czasie rzeczywistym dla zdarzeń IME/dotyk (przydatne do diagnozowania problemów na Androidzie).",
"keywords_summary": "Słowa kluczowe rozpoznawane przez parser OCR",
"keywords_desc": "Wpisz te słowa kluczowe w rysunku, aby wygenerować odpowiednią strukturę markdown. Wszystkie są niewrażliwe na wielkość liter (//h1 = //H1). Treść następuje po słowie kluczowym oddzielona spacją.",
"keywords_col_keyword": "Słowo kluczowe",
"keywords_col_syntax": "Składnia",
"keywords_col_output": "Wynik",
"menu_expand_all": "Rozwiń wszystkie rysunki",
"menu_collapse_all": "Zwiń wszystkie rysunki",
"menu_convert_all": "Konwertuj wszystkie rysunki na tekst",
"error_conversion": "Błąd konwersji: ",
"btn_pen": "Pióro",
"btn_eraser": "Gumka",
"btn_undo": "Cofnij",
"btn_redo": "Ponów",
"btn_clear": "Wyczyść wszystko",
"btn_convert": "Konwertuj do Markdown",
"btn_save": "Zapisz",
"btn_delete": "Usuń blok",
"btn_close": "Zamknij",
"btn_collapse": "Zwiń",
"btn_expand": "Rozwiń",
"btn_open_editor": "Otwórz edytor rysunku",
"confirm_delete": "Usunąć ten blok pisma odręcznego i powiązany plik SVG?",
"confirm_ok": "Usuń",
"confirm_cancel": "Anuluj"
}

View file

@ -0,0 +1,50 @@
{
"kw_title": "Título",
"kw_text": "texto",
"kw_footnote": "nota de rodapé",
"kw_word": "palavra",
"ui_language_name": "Idioma da interface",
"ui_language_desc": "Idioma usado na página de configurações. \"Automático\" segue o idioma do sistema do Obsidian.",
"ui_language_auto": "Automático (idioma do sistema)",
"svg_folder_name": "Pasta SVG",
"svg_folder_desc": "Pasta no vault onde os arquivos SVG dos desenhos são salvos",
"canvas_width_name": "Largura do canvas",
"canvas_width_desc": "Resolução horizontal do canvas em pixels",
"canvas_height_name": "Altura do canvas",
"canvas_height_desc": "Resolução vertical do canvas em pixels",
"bg_mode_name": "Fundo do canvas",
"bg_mode_desc": "Cor de fundo da área de desenho. \"Automático\" segue o tema do Obsidian.",
"bg_mode_auto": "Automático (segue o tema do Obsidian)",
"bg_mode_light": "Claro (branco)",
"bg_mode_dark": "Escuro (cinza escuro)",
"gemini_key_name": "Chave de API do Gemini",
"gemini_key_desc": "Necessária para o reconhecimento OCR de manuscrito. Obtenha no Google AI Studio (aistudio.google.com).",
"ocr_langs_name": "Idiomas OCR",
"ocr_langs_desc": "Códigos de idioma BCP-47 separados por vírgula (ex: \"it, en, fr\"). Usados pelo modelo de IA para OCR.",
"debug_mode_name": "Modo debug",
"debug_mode_desc": "Mostra notificações em tempo real para eventos IME/toque (útil para diagnosticar problemas no Android).",
"keywords_summary": "Palavras-chave reconhecidas pelo parser OCR",
"keywords_desc": "Escreva estas palavras-chave no desenho para gerar a estrutura markdown correspondente. Todas são insensíveis a maiúsculas/minúsculas (//h1 = //H1). O conteúdo segue após a palavra-chave separado por um espaço.",
"keywords_col_keyword": "Palavra-chave",
"keywords_col_syntax": "Sintaxe",
"keywords_col_output": "Saída",
"menu_expand_all": "Expandir todos os desenhos",
"menu_collapse_all": "Recolher todos os desenhos",
"menu_convert_all": "Converter todos os desenhos em texto",
"error_conversion": "Erro na conversão: ",
"btn_pen": "Caneta",
"btn_eraser": "Borracha",
"btn_undo": "Desfazer",
"btn_redo": "Refazer",
"btn_clear": "Limpar tudo",
"btn_convert": "Converter para Markdown",
"btn_save": "Salvar",
"btn_delete": "Excluir bloco",
"btn_close": "Fechar",
"btn_collapse": "Recolher",
"btn_expand": "Expandir",
"btn_open_editor": "Abrir editor de desenho",
"confirm_delete": "Excluir este bloco de manuscrito e o arquivo SVG associado?",
"confirm_ok": "Excluir",
"confirm_cancel": "Cancelar"
}

View file

@ -0,0 +1,50 @@
{
"kw_title": "Заголовок",
"kw_text": "текст",
"kw_footnote": "сноска",
"kw_word": "слово",
"ui_language_name": "Язык интерфейса",
"ui_language_desc": "Язык страницы настроек. \"Авто\" следует системному языку Obsidian.",
"ui_language_auto": "Авто (системный язык)",
"svg_folder_name": "Папка SVG",
"svg_folder_desc": "Папка в хранилище для сохранения SVG-файлов рисунков",
"canvas_width_name": "Ширина холста",
"canvas_width_desc": "Горизонтальное разрешение холста в пикселях",
"canvas_height_name": "Высота холста",
"canvas_height_desc": "Вертикальное разрешение холста в пикселях",
"bg_mode_name": "Фон холста",
"bg_mode_desc": "Цвет фона области рисования. \"Авто\" следует теме Obsidian.",
"bg_mode_auto": "Авто (следует теме Obsidian)",
"bg_mode_light": "Светлый (белый)",
"bg_mode_dark": "Тёмный (тёмно-серый)",
"gemini_key_name": "API-ключ Gemini",
"gemini_key_desc": "Необходим для OCR-распознавания рукописного текста. Получите в Google AI Studio (aistudio.google.com).",
"ocr_langs_name": "Языки OCR",
"ocr_langs_desc": "Коды языков BCP-47, разделённые запятой (например \"it, en, fr\"). Используются моделью ИИ для OCR.",
"debug_mode_name": "Режим отладки",
"debug_mode_desc": "Показывать уведомления в реальном времени для событий IME/касания (полезно для диагностики проблем на Android).",
"keywords_summary": "Ключевые слова, распознаваемые парсером OCR",
"keywords_desc": "Пишите эти ключевые слова в рисунке для создания соответствующей структуры markdown. Регистр не важен (//h1 = //H1). Содержимое следует за ключевым словом через пробел.",
"keywords_col_keyword": "Ключевое слово",
"keywords_col_syntax": "Синтаксис",
"keywords_col_output": "Вывод",
"menu_expand_all": "Развернуть все рисунки",
"menu_collapse_all": "Свернуть все рисунки",
"menu_convert_all": "Конвертировать все рисунки в текст",
"error_conversion": "Ошибка конвертации: ",
"btn_pen": "Ручка",
"btn_eraser": "Ластик",
"btn_undo": "Отменить",
"btn_redo": "Повторить",
"btn_clear": "Очистить всё",
"btn_convert": "Конвертировать в Markdown",
"btn_save": "Сохранить",
"btn_delete": "Удалить блок",
"btn_close": "Закрыть",
"btn_collapse": "Свернуть",
"btn_expand": "Развернуть",
"btn_open_editor": "Открыть редактор рисунков",
"confirm_delete": "Удалить этот блок рукописного ввода и связанный файл SVG?",
"confirm_ok": "Удалить",
"confirm_cancel": "Отмена"
}

View file

@ -0,0 +1,50 @@
{
"kw_title": "标题",
"kw_text": "文本",
"kw_footnote": "脚注",
"kw_word": "词语",
"ui_language_name": "界面语言",
"ui_language_desc": "设置页面使用的语言。\"自动\"跟随Obsidian系统语言。",
"ui_language_auto": "自动(系统语言)",
"svg_folder_name": "SVG文件夹",
"svg_folder_desc": "保存绘图SVG文件的vault文件夹",
"canvas_width_name": "画布宽度",
"canvas_width_desc": "画布水平分辨率(像素)",
"canvas_height_name": "画布高度",
"canvas_height_desc": "画布垂直分辨率(像素)",
"bg_mode_name": "画布背景",
"bg_mode_desc": "绘图区域的背景颜色。\"自动\"跟随Obsidian主题。",
"bg_mode_auto": "自动跟随Obsidian主题",
"bg_mode_light": "浅色(白色)",
"bg_mode_dark": "深色(深灰色)",
"gemini_key_name": "Gemini API密钥",
"gemini_key_desc": "手写OCR识别所需。从Google AI Studio (aistudio.google.com)获取。",
"ocr_langs_name": "OCR语言",
"ocr_langs_desc": "逗号分隔的BCP-47语言代码例如\"it, en, fr\"。用于AI模型的OCR识别。",
"debug_mode_name": "调试模式",
"debug_mode_desc": "显示IME/触摸事件的实时通知用于诊断Android问题。",
"keywords_summary": "OCR解析器识别的关键词",
"keywords_desc": "在绘图中写入这些关键词以生成相应的markdown结构。不区分大小写//h1 = //H1。内容跟在关键词后面用空格分隔。",
"keywords_col_keyword": "关键词",
"keywords_col_syntax": "语法",
"keywords_col_output": "输出",
"menu_expand_all": "展开所有绘图",
"menu_collapse_all": "折叠所有绘图",
"menu_convert_all": "将所有绘图转换为文本",
"error_conversion": "转换错误:",
"btn_pen": "画笔",
"btn_eraser": "橡皮擦",
"btn_undo": "撤销",
"btn_redo": "重做",
"btn_clear": "全部清除",
"btn_convert": "转换为Markdown",
"btn_save": "保存",
"btn_delete": "删除块",
"btn_close": "关闭",
"btn_collapse": "折叠",
"btn_expand": "展开",
"btn_open_editor": "打开绘图编辑器",
"confirm_delete": "删除此手写块及关联的SVG文件",
"confirm_ok": "删除",
"confirm_cancel": "取消"
}

View file

@ -7,7 +7,8 @@
- Tab impostazioni
============================================= */
import { Plugin, TFile, Notice } from 'obsidian';
import { Plugin, TFile, TFolder, Notice, FuzzySuggestModal, FuzzyMatch, MarkdownView, Editor } from 'obsidian';
import { t, setLocale } from './i18n';
import { DEFAULT_SETTINGS, HandwritingSettings, HandwritingSettingTab } from './settings';
import { registerEmbed, insertHandwritingBlock } from './embed';
import { VIEW_TYPE_HANDWRITING, DrawingEditorView } from './editor-view';
@ -47,6 +48,9 @@ export default class HandwritingPlugin extends Plugin {
async onload() {
await this.loadSettings();
// Applica la lingua interfaccia salvata (o la lingua di sistema se 'auto')
setLocale(this.settings.uiLanguage);
// Rileva cambio tema Obsidian (aggiunta/rimozione classe 'theme-dark' sul body).
// Se bgMode è 'auto', notifica tutti i listener per aggiornare colori e SVG.
const themeObserver = new MutationObserver(() => {
@ -65,11 +69,22 @@ export default class HandwritingPlugin extends Plugin {
this.addCommand({
id: 'insert-handwriting',
name: 'Insert handwriting block',
icon: 'pencil',
editorCallback: async () => {
await insertHandwritingBlock(this);
}
});
// Comando: inserisce un riferimento a un SVG esistente nella cartella handwriting
this.addCommand({
id: 'insert-svg-reference',
name: 'Insert SVG reference',
icon: 'file-plus',
editorCallback: (editor: Editor) => {
new SvgReferenceSuggest(this.app, this, editor).open();
}
});
// Icona nella ribbon (sidebar sinistra)
this.addRibbonIcon('pencil', 'Insert handwriting', async () => {
await insertHandwritingBlock(this);
@ -86,7 +101,7 @@ export default class HandwritingPlugin extends Plugin {
this.app.workspace.on('file-menu', (menu, file) => {
if (!(file instanceof TFile) || file.extension !== 'md') return;
menu.addItem(item => item
.setTitle('Espandi tutti i disegni')
.setTitle(t('menu_expand_all'))
.setIcon('chevrons-down')
.setSection('danger')
.onClick(() => {
@ -94,7 +109,7 @@ export default class HandwritingPlugin extends Plugin {
})
);
menu.addItem(item => item
.setTitle('Collassa tutti i disegni')
.setTitle(t('menu_collapse_all'))
.setIcon('chevrons-up')
.setSection('danger')
.onClick(() => {
@ -102,7 +117,7 @@ export default class HandwritingPlugin extends Plugin {
})
);
menu.addItem(item => item
.setTitle('Converti tutti i disegni in testo')
.setTitle(t('menu_convert_all'))
.setIcon('file-text')
.setSection('danger')
.onClick(async () => {
@ -112,7 +127,7 @@ export default class HandwritingPlugin extends Plugin {
await actions.convert();
}
} catch (e: unknown) {
new Notice('Errore nella conversione: ' + (e instanceof Error ? e.message : String(e)));
new Notice(t('error_conversion') + (e instanceof Error ? e.message : String(e)));
}
})
);
@ -156,3 +171,48 @@ export default class HandwritingPlugin extends Plugin {
await this.saveData(this.settings);
}
}
// Modal fuzzy-search per selezionare un SVG esistente nella cartella handwriting
// e inserire il riferimento ![[path]] nel cursore dell'editor attivo.
class SvgReferenceSuggest extends FuzzySuggestModal<TFile> {
constructor(
app: import('obsidian').App,
private plugin: HandwritingPlugin,
private editor: Editor
) {
super(app);
this.setPlaceholder('Cerca SVG...');
}
// Restituisce tutti gli SVG nella cartella impostata (esclusa _converted)
getItems(): TFile[] {
const folder = this.app.vault.getAbstractFileByPath(this.plugin.settings.svgFolder);
if (!(folder instanceof TFolder)) return [];
return folder.children.filter(
(f): f is TFile =>
f instanceof TFile &&
f.extension === 'svg' &&
!f.path.includes('/_converted/')
);
}
// Testo usato per il fuzzy-match (nome file)
getItemText(file: TFile): string {
return file.name;
}
// Mostra thumbnail SVG + nome file invece del solo testo
renderSuggestion(match: FuzzyMatch<TFile>, el: HTMLElement): void {
const file = match.item;
el.addClass('hwm_svg-suggest-item');
// Thumbnail SVG tramite resource URL del vault
const img = el.createEl('img', { cls: 'hwm_svg-thumb' });
img.src = this.app.vault.getResourcePath(file);
el.createEl('span', { text: file.name, cls: 'hwm_svg-suggest-name' });
}
// Inserisce ![[path]] al cursore quando l'utente seleziona un file
onChooseItem(file: TFile): void {
this.editor.replaceSelection(`![[${file.path}]]`);
}
}

View file

@ -4,6 +4,7 @@
import { App, PluginSettingTab, Setting } from 'obsidian';
import type HandwritingPlugin from './main';
import { t, setLocale, availableLocales, localeNames } from './i18n';
// Modalità sfondo: chiaro, scuro o automatico (segue il tema di Obsidian)
export type BgMode = 'light' | 'dark' | 'auto';
@ -16,6 +17,7 @@ export interface HandwritingSettings {
ocrLanguages: string[]; // lingue per il riconoscimento OCR (codici BCP-47, es. 'it', 'en')
geminiApiKey: string; // chiave API Google Gemini per l'OCR
debugMode: boolean; // mostra Notice di debug per eventi IME/touch
uiLanguage: string; // lingua dell'interfaccia impostazioni ('auto' = segue sistema)
}
// Colori predefiniti per le modalità light e dark
@ -79,6 +81,7 @@ export const DEFAULT_SETTINGS: HandwritingSettings = {
ocrLanguages: ['it', 'en'], // italiano e inglese di default
geminiApiKey: '',
debugMode: false,
uiLanguage: 'auto', // default: segue la lingua di sistema di Obsidian
};
// Nome del branch corrente — aggiornare manualmente ad ogni cambio di branch
@ -98,16 +101,35 @@ export class HandwritingSettingTab extends PluginSettingTab {
containerEl.createEl('h2', { text: 'Handwriting to Markdown' });
// Riga versione + branch
// Riga versione
containerEl.createEl('p', {
text: `v${this.plugin.manifest.version} — branch: ${PLUGIN_BRANCH}`,
text: `v${this.plugin.manifest.version}`,
cls: 'setting-item-description',
});
// --- Lingua interfaccia ---
new Setting(containerEl)
.setName(t('ui_language_name'))
.setDesc(t('ui_language_desc'))
.addDropdown(drop => {
// Prima voce: automatico
drop.addOption('auto', t('ui_language_auto'));
// Una voce per ogni lingua disponibile nel plugin, con nome nativo
availableLocales().forEach(code => drop.addOption(code, localeNames[code] ?? code));
drop.setValue(this.plugin.settings.uiLanguage);
drop.onChange(async (value) => {
this.plugin.settings.uiLanguage = value;
await this.plugin.saveSettings();
// Aggiorna il dizionario attivo e ridisegna la pagina impostazioni
setLocale(value);
this.display();
});
});
// --- Cartella SVG ---
new Setting(containerEl)
.setName('Cartella SVG')
.setDesc('Cartella nel vault dove vengono salvati i file SVG dei disegni')
.setName(t('svg_folder_name'))
.setDesc(t('svg_folder_desc'))
.addText(text => text
.setPlaceholder('_handwriting')
.setValue(this.plugin.settings.svgFolder)
@ -118,8 +140,8 @@ export class HandwritingSettingTab extends PluginSettingTab {
// --- Larghezza canvas ---
new Setting(containerEl)
.setName('Larghezza canvas')
.setDesc('Risoluzione orizzontale del canvas in pixel')
.setName(t('canvas_width_name'))
.setDesc(t('canvas_width_desc'))
.addText(text => text
.setValue(String(this.plugin.settings.canvasWidth))
.onChange(async (value) => {
@ -132,8 +154,8 @@ export class HandwritingSettingTab extends PluginSettingTab {
// --- Altezza canvas ---
new Setting(containerEl)
.setName('Altezza canvas')
.setDesc('Risoluzione verticale del canvas in pixel')
.setName(t('canvas_height_name'))
.setDesc(t('canvas_height_desc'))
.addText(text => text
.setValue(String(this.plugin.settings.canvasHeight))
.onChange(async (value) => {
@ -146,12 +168,12 @@ export class HandwritingSettingTab extends PluginSettingTab {
// --- Sfondo canvas ---
new Setting(containerEl)
.setName('Sfondo canvas')
.setDesc('Colore di sfondo del riquadro di disegno. "Automatico" segue il tema di Obsidian.')
.setName(t('bg_mode_name'))
.setDesc(t('bg_mode_desc'))
.addDropdown(drop => drop
.addOption('auto', 'Automatico (segue tema Obsidian)')
.addOption('light', 'Chiaro (bianco)')
.addOption('dark', 'Scuro (grigio scuro)')
.addOption('auto', t('bg_mode_auto'))
.addOption('light', t('bg_mode_light'))
.addOption('dark', t('bg_mode_dark'))
.setValue(this.plugin.settings.bgMode)
.onChange(async (value) => {
this.plugin.settings.bgMode = value as BgMode;
@ -162,8 +184,8 @@ export class HandwritingSettingTab extends PluginSettingTab {
// --- Chiave API Gemini ---
new Setting(containerEl)
.setName('Chiave API Gemini')
.setDesc('Necessaria per il riconoscimento OCR della scrittura a mano. Ottienila da Google AI Studio (aistudio.google.com).')
.setName(t('gemini_key_name'))
.setDesc(t('gemini_key_desc'))
.addText(text => {
text
.setPlaceholder('AIza...')
@ -178,11 +200,8 @@ export class HandwritingSettingTab extends PluginSettingTab {
// --- Lingue OCR ---
new Setting(containerEl)
.setName('Lingue OCR')
.setDesc(
'Codici lingua BCP-47 separati da virgola (es. "it, en, fr"). ' +
'Usati dal riconoscitore di scrittura su Android.'
)
.setName(t('ocr_langs_name'))
.setDesc(t('ocr_langs_desc'))
.addText(text => text
// Mostra l'array come stringa "it, en"
.setValue(this.plugin.settings.ocrLanguages.join(', '))
@ -197,70 +216,66 @@ export class HandwritingSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
}));
// --- Modalità debug ---
new Setting(containerEl)
.setName('Modalità debug')
.setDesc('Mostra notifiche in tempo reale per eventi IME/touch (utile per diagnosticare problemi su Android).')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.debugMode)
.onChange(async (value) => {
this.plugin.settings.debugMode = value;
await this.plugin.saveSettings();
}));
// --- Modalità debug (nascosta dall'UI, funzionalità mantenuta) ---
// --- Riferimento keyword OCR (sezione espandibile) ---
// NOTA SVILUPPATORI: se aggiungi una keyword in md-parser.ts, aggiornala anche qui!
const details = containerEl.createEl('details', { cls: 'hwm_keyword-ref' });
details.createEl('summary', {
text: 'Keyword riconosciute dal parser OCR',
text: t('keywords_summary'),
cls: 'hwm_keyword-summary',
});
details.createEl('p', {
text: 'Scrivi queste keyword nel disegno per generare la struttura markdown corrispondente. Tutte sono case-insensitive (//h1 = //H1). Il contenuto segue dopo la keyword separato da uno spazio.',
text: t('keywords_desc'),
cls: 'setting-item-description',
});
// Tabella keyword: [nome, sintassi, output]
// Le parole segnaposto (titolo, testo, ecc.) vengono tradotte via t()
const T = t('kw_title'); // es. "Titolo" / "Title"
const X = t('kw_text'); // es. "testo" / "text"
const FN = t('kw_footnote'); // es. "nota a piè pagina" / "footnote"
const W = t('kw_word'); // es. "parola" / "word"
const KEYWORDS: [string, string, string][] = [
['//H1', '//H1 Titolo', '# Titolo'],
['//H2', '//H2 Titolo', '## Titolo'],
['//H3', '//H3 Titolo', '### Titolo'],
['//H4', '//H4 Titolo', '#### Titolo'],
['//B / //BOLD', '//B testo', '**testo**'],
['//I', '//I testo', '*testo*'],
['//BI', '//BI testo', '***testo***'],
['//S / //STRIKE', '//S testo', '~~testo~~'],
['//HL', '//HL testo', '==testo=='],
['//CODE', '//CODE testo', '`testo`'],
['//H1', `//H1 ${T}`, `# ${T}`],
['//H2', `//H2 ${T}`, `## ${T}`],
['//H3', `//H3 ${T}`, `### ${T}`],
['//H4', `//H4 ${T}`, `#### ${T}`],
['//B / //BOLD', `//B ${X}`, `**${X}**`],
['//I', `//I ${X}`, `*${X}*`],
['//BI', `//BI ${X}`, `***${X}***`],
['//S / //STRIKE', `//S ${X}`, `~~${X}~~`],
['//HL', `//HL ${X}`, `==${X}==`],
['//CODE', `//CODE ${X}`, `\`${X}\``],
['//CODEBLOCK', '//CODEBLOCK js', '```js\n...\n```'],
['//LIST', '//LIST a, b, c', '- a\n- b\n- c'],
['//NUMLIST', '//NUMLIST a, b, c', '1. a\n2. b\n3. c'],
['//CHECK', '//CHECK a, b, c', '- [ ] a\n- [ ] b\n- [ ] c'],
['//TABLE', '//TABLE Col1, Col2', '| Col1 | Col2 |\n|---|---|\n| ... |'],
['//NOTE', '//NOTE testo', '> [!NOTE]\n> testo'],
['//WARN', '//WARN testo', '> [!WARNING]\n> testo'],
['//TIP', '//TIP testo', '> [!TIP]\n> testo'],
['//INFO', '//INFO testo', '> [!INFO]\n> testo'],
['//ERROR', '//ERROR testo', '> [!ERROR]\n> testo'],
['//IMPORTANT', '//IMPORTANT testo', '> [!IMPORTANT]\n> testo'],
['//QUOTE', '//QUOTE testo', '> testo'],
['//LINK', '//LINK testo, url', '[testo](url)'],
['//NOTE', `//NOTE ${X}`, `> [!NOTE]\n> ${X}`],
['//WARN', `//WARN ${X}`, `> [!WARNING]\n> ${X}`],
['//TIP', `//TIP ${X}`, `> [!TIP]\n> ${X}`],
['//INFO', `//INFO ${X}`, `> [!INFO]\n> ${X}`],
['//ERROR', `//ERROR ${X}`, `> [!ERROR]\n> ${X}`],
['//IMPORTANT', `//IMPORTANT ${X}`, `> [!IMPORTANT]\n> ${X}`],
['//QUOTE', `//QUOTE ${X}`, `> ${X}`],
['//LINK', `//LINK ${X}, url`, `[${X}](url)`],
['//IMG', '//IMG alt, url', '![alt](url)'],
['//HR / //SEP', '//HR', '---'],
['//FN', '//FN nota a piè pagina', '[^1]: nota a piè pagina'],
['//FN', `//FN ${FN}`, `[^1]: ${FN}`],
['//MATH', '//MATH formula', '$formula$'],
['//MATHBLOCK', '//MATHBLOCK', '$$\n...\n$$'],
['//TAG', '//TAG parola', '#parola'],
['//TAG', `//TAG ${W}`, `#${W}`],
['//DATE', '//DATE', 'YYYY-MM-DD'],
['//TIME', '//TIME', 'HH:mm'],
['//DATETIME', '//DATETIME', 'YYYY-MM-DD HH:mm'],
['//INDENT', '//INDENT testo', ' testo'],
['//INDENT', `//INDENT ${X}`, ` ${X}`],
];
const table = details.createEl('table', { cls: 'hwm_keyword-table' });
const thead = table.createEl('thead');
const hrow = thead.createEl('tr');
['Keyword', 'Sintassi', 'Output'].forEach(h => hrow.createEl('th', { text: h }));
[t('keywords_col_keyword'), t('keywords_col_syntax'), t('keywords_col_output')].forEach(h => hrow.createEl('th', { text: h }));
const tbody = table.createEl('tbody');
for (const [name, syntax, output] of KEYWORDS) {
const row = tbody.createEl('tr');

View file

@ -7,11 +7,14 @@
import { Point, Stroke } from './drawing-canvas';
// Genera ID univoco per nuovi disegni (timestamp + random)
// Genera ID univoco per nuovi disegni nel formato HTMD_YYYYMMDDHHMMSS_XXXX
export function generateId(): string {
const ts = Date.now().toString(36);
const rnd = Math.random().toString(36).substring(2, 6);
return `hw_${ts}_${rnd}`;
const now = new Date();
const p = (n: number) => String(n).padStart(2, '0');
const date = `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}`;
const time = `${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
const rnd = Math.random().toString(36).substring(2, 6).toUpperCase();
return `HTMD_${date}${time}_${rnd}`;
}
// Converte un array di punti in un attributo SVG path "d"

View file

@ -584,9 +584,67 @@
padding: 0 !important;
}
/* Topbar del DrawingModal: centra la toolbar (non c'è backBtn come nell'EditorView) */
/* Topbar centrata: usata sia nel DrawingModal (Windows) che in DrawingEditorView (Android).
position: relative serve ad ancorare il bottone chiudi (position: absolute) */
.hwm_editor-topbar--modal {
justify-content: center;
position: relative;
}
/* Bottone X chiudi: ancorato al bordo destro del topbar, indipendente dal toolbar centrato */
.hwm_close-btn {
position: absolute !important;
right: 8px;
top: 50%;
transform: translateY(-50%);
}
/* Nasconde la X nativa di Obsidian nel DrawingModal: la chiusura avviene dal bottone X in toolbar */
.hwm_modal .modal-close-button {
display: none !important;
}
/* Overlay di conferma inline — no Modal annidato, nessun furto di focus */
.hwm_confirm-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
z-index: 100; /* sopra il pannello portale (z-index: 10) */
border-radius: inherit;
pointer-events: auto !important; /* override pointer-events:none dello span padre */
}
.hwm_confirm-overlay button {
pointer-events: auto !important;
}
.hwm_confirm-msg {
color: #fff;
font-size: 14px;
}
/* --- Suggest SVG reference: thumbnail + nome file --- */
.hwm_svg-suggest-item {
display: flex;
align-items: center;
gap: 10px;
padding: 4px 0;
}
.hwm_svg-thumb {
width: 80px;
height: 48px;
object-fit: contain;
border-radius: 4px;
background: var(--background-secondary);
flex-shrink: 0;
}
.hwm_svg-suggest-name {
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* --- Scroll container per il canvas nell'editor --- */