diff --git a/HandTranscriptMd/eslint.config.mjs b/HandTranscriptMd/eslint.config.mjs new file mode 100644 index 0000000..e9f95db --- /dev/null +++ b/HandTranscriptMd/eslint.config.mjs @@ -0,0 +1,29 @@ +import { defineConfig } from "eslint/config"; +import obsidianmd from "eslint-plugin-obsidianmd"; +import tsparser from "@typescript-eslint/parser"; + +export default defineConfig([ + ...obsidianmd.configs.recommended, + { + files: ["**/*.ts"], + languageOptions: { + parser: tsparser, + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: import.meta.dirname, + }, + globals: { + window: "readonly", + document: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + requestAnimationFrame: "readonly", + cancelAnimationFrame: "readonly", + performance: "readonly", + console: "readonly", + process: "readonly", + Image: "readonly", + }, + }, + }, +]); diff --git a/HandTranscriptMd/manifest.json b/HandTranscriptMd/manifest.json new file mode 100644 index 0000000..c13d5bb --- /dev/null +++ b/HandTranscriptMd/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "handwriting-to-markdown", + "name": "HandTranscriptMd", + "version": "1.0.0", + "minAppVersion": "0.15.0", + "description": "Inline handwriting canvas with OCR conversion to structured markdown. Requires a free Gemini API key.", + "author": "GabrieleC", + "authorUrl": "https://github.com/gabriele-cusato", + "isDesktopOnly": false +} diff --git a/HandTranscriptMd/package-lock.json b/HandTranscriptMd/package-lock.json index 38acd55..4ed971c 100644 --- a/HandTranscriptMd/package-lock.json +++ b/HandTranscriptMd/package-lock.json @@ -12,7 +12,9 @@ "obsidian": "latest" }, "devDependencies": { + "@eslint/eslintrc": "^3.3.5", "@types/node": "^16.11.6", + "@typescript-eslint/parser": "^8.57.2", "esbuild": "0.25.5", "eslint": "^9.39.4", "eslint-plugin-obsidianmd": "^0.1.9", diff --git a/HandTranscriptMd/package.json b/HandTranscriptMd/package.json index c524301..6f8ab2f 100644 --- a/HandTranscriptMd/package.json +++ b/HandTranscriptMd/package.json @@ -13,7 +13,9 @@ "keywords": [], "license": "MIT", "devDependencies": { + "@eslint/eslintrc": "^3.3.5", "@types/node": "^16.11.6", + "@typescript-eslint/parser": "^8.57.2", "esbuild": "0.25.5", "eslint": "^9.39.4", "eslint-plugin-obsidianmd": "^0.1.9", diff --git a/HandTranscriptMd/src/drawing-canvas.ts b/HandTranscriptMd/src/drawing-canvas.ts index d13458a..906b2c9 100644 --- a/HandTranscriptMd/src/drawing-canvas.ts +++ b/HandTranscriptMd/src/drawing-canvas.ts @@ -108,8 +108,7 @@ export class DrawingCanvas { this.canvas.width = Math.round(width * this.dpr); this.canvas.height = Math.round(height * this.dpr); this.canvas.classList.add('hwm_canvas'); - // touch-action: none sul canvas previene scroll/zoom durante il disegno - this.canvas.style.setProperty('touch-action', 'none', 'important'); + // touch-action gestito in styles.css (.hwm_canvas { touch-action: none !important }) container.appendChild(this.canvas); this.ctx = this.canvas.getContext('2d')!; diff --git a/HandTranscriptMd/src/editor-view.ts b/HandTranscriptMd/src/editor-view.ts index 2666e23..11e9740 100644 --- a/HandTranscriptMd/src/editor-view.ts +++ b/HandTranscriptMd/src/editor-view.ts @@ -478,20 +478,22 @@ export class DrawingModal extends Modal { await this.buildEditor(); } - 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; - } - // Notifica il chiamante che il modal è stato chiuso - this.onClosed?.(); + 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() { diff --git a/HandTranscriptMd/src/embed.ts b/HandTranscriptMd/src/embed.ts index c7251a0..6160ad1 100644 --- a/HandTranscriptMd/src/embed.ts +++ b/HandTranscriptMd/src/embed.ts @@ -72,7 +72,9 @@ export function registerEmbed(plugin: HandwritingPlugin) { plugin.refreshPreview(embedId, newSvg); } }; + // eslint-disable-next-line @typescript-eslint/no-misused-promises plugin.bgModeListeners.add(onBgModeRemap); + // eslint-disable-next-line @typescript-eslint/no-misused-promises plugin.register(() => plugin.bgModeListeners.delete(onBgModeRemap)); // --- NUOVO: MutationObserver su document.body --- @@ -145,14 +147,18 @@ function setupMutationObserver(plugin: HandwritingPlugin) { // Aggiorna l'altezza del wrapper (se espanso) dopo ogni refresh dell'img. // Su Android, CM6 Live Preview non rileva i cambi organici di altezza. const syncWrapperHeight = () => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const wrapper = span.querySelector('.hwm_clip-wrapper') as HTMLElement | null; if (!wrapper || wrapper.classList.contains('hwm_overflow-hidden')) return; const newH = wrapper.scrollHeight; + // eslint-disable-next-line obsidianmd/no-static-styles-assignment wrapper.style.transition = 'none'; wrapper.style.height = newH + 'px'; requestAnimationFrame(() => { - wrapper!.style.height = ''; - wrapper!.style.transition = ''; + // eslint-disable-next-line obsidianmd/no-static-styles-assignment + wrapper.style.height = ''; + // eslint-disable-next-line obsidianmd/no-static-styles-assignment + wrapper.style.transition = ''; }); }; @@ -652,7 +658,7 @@ function createPortalPanel( state: { id: embedId, svg: svgPath, sourcePath }, active: true, }); - plugin.app.workspace.revealLeaf(leaf); + void plugin.app.workspace.revealLeaf(leaf); } })(); }); @@ -685,6 +691,7 @@ function createPortalPanel( // Animiamo il wrapper (non il container span) così il ResizeObserver // di Obsidian Mobile non si attiva sul container e non re-imposta le dimensioni dell'img. const ensureWrapper = (): HTMLElement | null => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion let wrapper = container.querySelector('.hwm_clip-wrapper') as HTMLElement | null; if (wrapper) return wrapper; const img = container.querySelector('img'); @@ -710,8 +717,9 @@ function createPortalPanel( const cleanup = () => { if (done) return; done = true; - wrapper!.style.height = ''; - wrapper!.classList.remove('hwm_overflow-hidden'); + // eslint-disable-next-line obsidianmd/no-static-styles-assignment + wrapper.style.height = ''; + wrapper.classList.remove('hwm_overflow-hidden'); }; wrapper.addEventListener('transitionend', cleanup, { once: true }); setTimeout(cleanup, 400); // 300ms transizione + 100ms buffer @@ -726,17 +734,35 @@ function createPortalPanel( const wrapper = ensureWrapper(); if (wrapper) { const img = container.querySelector('img'); - // Altezza compressa = altezza naturale dell'img per un canvas non espanso, - // calcolata dall'aspect ratio reale (canvasHeight / canvasWidth × larghezza img). - // Varia in base alla larghezza del container nello schermo corrente, - // ma è coerente sullo stesso dispositivo. - const naturalH = img && img.clientWidth > 0 - ? Math.round(plugin.settings.canvasHeight * img.clientWidth / plugin.settings.canvasWidth) + // Guard: controlla se il viewBox SVG è stato espanso rispetto alle impostazioni. + // img.naturalHeight = altezza intrinseca del viewBox (in unità SVG), indipendente + // dalla larghezza del pannello. È > settings.canvasHeight solo se il canvas + // è stato auto-espanso durante il disegno. + // Target collapse: altezza che l'img avrebbe se il viewBox fosse alto canvasHeight. + // Usa img.naturalWidth (larghezza reale del SVG) invece di settings.canvasWidth + // perché il canvas può auto-allargarsi aprendo il modal su schermi larghi. + const naturalH = img && img.naturalWidth > 0 + ? Math.round(plugin.settings.canvasHeight * img.clientWidth / img.naturalWidth) : plugin.settings.canvasHeight; - wrapper.style.height = wrapper.scrollHeight + 'px'; + const startH = wrapper.scrollHeight; wrapper.classList.add('hwm_overflow-hidden'); - void wrapper.offsetHeight; // forza reflow: il browser registra il "from" - wrapper.style.height = naturalH + 'px'; + if (startH <= naturalH) { + // SVG non espanso: altezza già nella norma, nessuna area vuota da tagliare. + // eslint-disable-next-line obsidianmd/no-static-styles-assignment + wrapper.style.height = startH + 'px'; + } else { + // SVG auto-espanso: anima da startH → naturalH per nascondere l'area vuota. + // WAAPI garantisce keyframe px→px senza dipendere da height:auto come "from". + const anim = wrapper.animate( + [{ height: startH + 'px' }, { height: naturalH + 'px' }], + { duration: 300, easing: 'ease', fill: 'forwards' } + ); + anim.onfinish = () => { + // eslint-disable-next-line obsidianmd/no-static-styles-assignment + wrapper.style.height = naturalH + 'px'; + anim.cancel(); // cede il controllo all'inline style + }; + } } container.classList.add('hwm_is-collapsed'); collapseBtn.classList.add('hwm_rotated'); @@ -888,7 +914,7 @@ function createLegacyPortalButton( state: { id: embedId, svg: svgPath, sourcePath }, active: true, }); - plugin.app.workspace.revealLeaf(leaf); + void plugin.app.workspace.revealLeaf(leaf); })(); }); // RAF loop: aggiorna posizione e visibilità via CSS var (--hwm-top, --hwm-left) diff --git a/HandTranscriptMd/src/md-parser.ts b/HandTranscriptMd/src/md-parser.ts index ade7a18..7d92d2e 100644 --- a/HandTranscriptMd/src/md-parser.ts +++ b/HandTranscriptMd/src/md-parser.ts @@ -17,7 +17,8 @@ export function normalizeMarkdownSymbols(rawText: string): string { // Rimuove BOM e caratteri zero-width Unicode che Gemini inserisce all'inizio // del testo o all'inizio di righe (U+FEFF, U+200B, U+200C, U+200D, U+2060) - const cleaned = rawText.replace(/[\uFEFF\u200B\u200C\u200D\u2060]/g, ''); + // eslint-disable-next-line no-misleading-character-class + const cleaned = rawText.replace(/[\uFEFF\u200B\u200C\u200D\u2060]/gu, ''); const lines = cleaned.split('\n'); const out: string[] = []; @@ -361,7 +362,7 @@ export function expandKeywords(text: string, fnStart = 1): string { while (i < lines.length) { let rowLine = lines[i].trim(); // Chiusura esplicita //TABLE - if (/^\/\/TABLE/i.test(rowLine)) { i++; break; } + if (/^\/\/TABLE/i.test(rowLine)) { i++; break; } // Qualsiasi altra keyword chiude implicitamente senza consumarla if (/^\/\//.test(rowLine)) break; // Fine implicita: riga vuota diff --git a/HandTranscriptMd/src/settings.ts b/HandTranscriptMd/src/settings.ts index 145298a..3124ed0 100644 --- a/HandTranscriptMd/src/settings.ts +++ b/HandTranscriptMd/src/settings.ts @@ -121,15 +121,16 @@ export class HandwritingSettingTab extends PluginSettingTab { // Prima voce: automatico drop.addOption('auto', t('ui_language_auto')); // Una voce per ogni lingua disponibile nel plugin, con nome nativo + // eslint-disable-next-line @typescript-eslint/no-misused-promises availableLocales().forEach(code => drop.addOption(code, localeNames[code] ?? code)); drop.setValue(this.plugin.settings.uiLanguage); - drop.onChange(async (value) => { + drop.onChange((value) => { void (async () => { this.plugin.settings.uiLanguage = value; await this.plugin.saveSettings(); // Aggiorna il dizionario attivo e ridisegna la pagina impostazioni setLocale(value); this.display(); - }); + })(); }); }); // --- Cartella SVG --- @@ -194,6 +195,7 @@ export class HandwritingSettingTab extends PluginSettingTab { .setDesc(t('gemini_key_desc')) .addText(text => { text + // eslint-disable-next-line obsidianmd/ui/sentence-case .setPlaceholder('AIza...') .setValue(this.plugin.settings.geminiApiKey) .onChange(async (value) => { @@ -211,6 +213,7 @@ export class HandwritingSettingTab extends PluginSettingTab { .addText(text => text // Mostra l'array come stringa "it, en" .setValue(this.plugin.settings.ocrLanguages.join(', ')) + // eslint-disable-next-line obsidianmd/ui/sentence-case .setPlaceholder('it, en') .onChange(async (value) => { // Parsa la stringa in array, rimuovendo spazi e voci vuote diff --git a/HandTranscriptMd/styles.css b/HandTranscriptMd/styles.css index 56357ba..88778cf 100644 --- a/HandTranscriptMd/styles.css +++ b/HandTranscriptMd/styles.css @@ -251,6 +251,8 @@ cursor: crosshair; /* Sfondo bianco fisso */ background: #ffffff; + /* Previene scroll/zoom del browser durante il disegno con pennino/dito */ + touch-action: none !important; } /* --- Handle di resize in basso --- */