From 315e5f597ea3631ab4b1ea9fb6b95d19b10aa29c Mon Sep 17 00:00:00 2001 From: gabriele-cusato Date: Mon, 23 Mar 2026 02:03:11 +0100 Subject: [PATCH] accettato il limite dell'handwriting, corretti vari bug --- CLAUDE.md | 117 ++++++++++++++++++++++-- HandTranscriptMd/src/editor-view.ts | 64 +++++++++++++ HandTranscriptMd/src/embed.ts | 137 +++++++++++++++++----------- HandTranscriptMd/src/main.ts | 12 +++ HandTranscriptMd/src/settings.ts | 2 + HandTranscriptMd/styles.css | 53 +++++++++-- 6 files changed, 318 insertions(+), 67 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9069745..3e99085 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,7 @@ Un plugin Obsidian che inserisce un **riquadro canvas inline** in un file `.md` --- -## Stato attuale — Fase 5 (fix overlay + centramento modal + badge mode + auto-scroll) — TESTATO +## Stato attuale — Fase 6 (pannello portale inline + bgMode live update + fix CSS tema) — TESTATO ### File del plugin (`HandTranscriptMd/src/`) @@ -50,7 +50,7 @@ Un plugin Obsidian che inserisce un **riquadro canvas inline** in un file `.md` | `settings.ts` | Impostazioni: cartella SVG, dimensioni canvas, sfondo, lingue OCR, chiave API Gemini, toggle `hwmHandwritingMode`; mostra versione (`plugin.manifest.version`) e branch (`PLUGIN_BRANCH` costante hardcoded) nell'header della pagina impostazioni | | `drawing-canvas.ts` | Motore disegno Canvas API: Bézier quadratiche, penna, gomma parziale, undo/redo history-based, auto-expand, righe foglio, `allowFingerScroll()` | | `svg-utils.ts` | Conversione tratti ↔ SVG (dati riedit in `` JSON), righe e sfondo inclusi nell'SVG | -| `embed.ts` | Code block processor → preview SVG inline + pannello portale in document.body; click matita → Modal (Windows) o nuova tab (Android); scroll fix; badge mode | +| `embed.ts` | Code block processor → preview SVG inline + pannello portale (figlio dello span, position:absolute); click matita → Modal (Windows) o nuova tab (Android); badge mode; `onBgModeRemap` per aggiornare SVG al cambio tema | | `editor-view.ts` | `DrawingEditorView` (tab dedicata, Android) + `DrawingModal` (overlay fullscreen, Windows), toolbar completa, auto-save | | `recognizer.ts` | `IRecognizer` interface + `GeminiRecognizer`: invia PNG base64 a Gemini API, restituisce testo | | `md-parser.ts` | `parseMarkdown()`: post-processa testo OCR riga per riga applicando sintassi markdown | @@ -66,8 +66,13 @@ Un plugin Obsidian che inserisce un **riquadro canvas inline** in un file `.md` - **MutationObserver su `document.body`** intercetta gli span con `src*="_handwriting/"` non appena appaiono nel DOM (funziona sia in reading view che in live preview dove il post-processor non viene chiamato) - `tryDecorate()` con flag `data-hwm-decorated="1"` per evitare doppia elaborazione; ritenta dopo 150ms se la classe `image-embed` non è ancora presente (caricamento asincrono) - **Classe badge mode**: `span.classList.toggle('hwm-badge-mode', hwmHandwritingMode)` — unica modifica allo span quando lo switch è attivo -- **Pannello portale** (`hwm_portal-panel`) in `document.body` con 4 bottoni: ✏️ (apre Modal su Windows / tab su Android), 📄 (converti OCR), ↕️ (comprimi/espandi), ✕ (elimina). Posizionato via RAF + `getBoundingClientRect()` + `position: fixed` -- **Scroll fix**: listener `scroll` su `.cm-scroller` / `.markdown-reading-view` che nasconde il pannello immediatamente durante lo scroll (visibilità ripristinata dopo 150ms) +- **Pannello portale** (`hwm_portal-panel`) con 4 bottoni: ✏️ (apre Modal su Windows / tab su Android), 📄 (converti OCR), ↕️ (comprimi/espandi), ✕ (elimina). + - **Posizione**: `container.appendChild(panel)` — il pannello è un figlio diretto dello span; `position: absolute; top: 6px; right: 6px` ancorato allo span (che ha `position: relative`). Non più in `document.body`. + - **Nessun RAF loop**: eliminato il loop `requestAnimationFrame` e il listener `scroll`. Il pannello segue lo span naturalmente nel DOM. + - **Cleanup**: `plugin.register(() => panel.remove())` — il pannello viene rimosso quando il plugin viene disabilitato. + - **Click dei bottoni**: lo span ha `pointer-events: none` (per handwriting Android); il pannello ha `pointer-events: auto` in CSS per ripristinare i click. + - **Nascondere con modal aperto**: Desktop → `panel.style.display = 'none'` nel click handler, ripristinato nel callback `modal.onClosed`; Mobile → `workspace.on('layout-change', ...)` per rilevare apertura/chiusura tab. +- **Comprimi/Espandi**: usa `container.style.height + overflow: hidden` sullo span contenitore — modifica solo l'altezza visibile senza toccare la larghezza dell'immagine. - **Refresh immagine**: `previewCallbacks.set(embedId, ...)` aggiorna `img.src` con cache-bust `?t=timestamp` dopo ogni salvataggio dalla tab editor (Obsidian non aggiorna automaticamente `![[svg]]` in live preview quando il file cambia) - **NO data-URI**: il src dell'img resta sempre l'URL vault (`http://localhost/_capacitor_file_/...`) — un data-URI verrebbe interpretato da Android come drawing surface @@ -88,8 +93,9 @@ Un plugin Obsidian che inserisce un **riquadro canvas inline** in un file `.md` `DrawingModal extends Modal` — usato su **Windows**: - Overlay fullscreen (`hwm_modal`: `95vw × 90vh`) che appare sopra il documento - Stessa toolbar e stesso canvas di `DrawingEditorView` -- `onClosed?: () => void` — callback invocato alla chiusura per aggiornare il flag `modalOpen` nel RAF loop del pannello portale +- `onClosed?: () => void` — callback invocato alla chiusura per ripristinare `panel.style.display` nel pannello portale - `replaceCodeBlock` / `removeCodeBlock` gestiscono sia formato wiki `![[svg]]` che legacy (prova prima wiki, poi code block come fallback) +- `private bgModeListener` registrato in `buildEditor()` (dopo `colorBtns`) e rimosso in `onClose()` — aggiorna topbar, toolbar, pallini colore e canvas al cambio bgMode in tempo reale (utile se le impostazioni fossero accessibili con modal aperto) **Comportamento bottone matita (pannello portale):** - `Platform.isDesktop` → `new DrawingModal(...).open()` — si apre nella stessa finestra @@ -124,7 +130,7 @@ Un plugin Obsidian che inserisce un **riquadro canvas inline** in un file `.md` - **Settings OCR** — chiave API Gemini (campo password) + lingue OCR configurabili (default: `it, en`) - **Supporto Android** — penna disegna, dito scrolla, nessun conflitto handwriting - **Comprimi/Espandi** — preview inline si può compattare all'altezza di default (freccia con rotazione 180°) -- **Bottone portale** — pannello floating in `document.body`, si posiziona sopra il riquadro via RAF + getBoundingClientRect, scompare durante lo scroll (listener scroll + visibility hidden/visible) +- **Bottone portale** — pannello `position: absolute` figlio diretto dello span (no RAF loop, no scroll listener, no document.body) - **Preview non tappabile** — click handler rimosso; l'unico modo per aprire l'editor è il bottone matita nel pannello portale - **Modal Windows** — click matita su Desktop apre `DrawingModal` (overlay fullscreen nella stessa finestra, no tab separata) - **Nuova tab Android** — click matita su Mobile apre `DrawingEditorView` in tab separata @@ -252,6 +258,72 @@ background: var(--background-secondary); border-radius: 6px; Più: `img { display: none !important }` per nascondere l'SVG e `::after { content: "✏️"; font-size: 28px; opacity: 0.5; }` per l'icona. **File**: `styles.css`. +### ✅ TEST EFFETTUATI (sessione 2026-03-23) + +#### Bug 5 — Pannello portale visibile nelle impostazioni e ovunque — RISOLTO ✅ + +**Sintomo**: il pannello portale (`position: fixed` in `document.body`) rimaneva visibile in alto a destra anche navigando nelle impostazioni, in altre schede, ecc. +**Causa**: il pannello era appeso a `document.body` e il RAF loop di posizionamento lo seguiva solo quando lo span era nel viewport. +**Fix**: pannello spostato come figlio diretto dello span contenitore con `position: absolute; top: 6px; right: 6px`. Lo span ha `position: relative`. Il pannello è ora parte del DOM del documento e scompare naturalmente quando si naviga altrove. +**File**: `src/embed.ts` — `createPortalPanel()` + `styles.css`. + +--- + +#### Bug 6 — Comprimi/Espandi modificava anche la larghezza — RISOLTO ✅ + +**Sintomo**: cliccando il bottone freccia per comprimere/espandere il riquadro, anche la larghezza cambiava (l'immagine si restringeva). +**Causa**: il codice precedente usava `max-height` sull'elemento ``, che lo scalava proporzionalmente. +**Fix**: la compressione ora modifica `container.style.height` + `overflow: hidden` sullo span contenitore. L'immagine viene clippata verticalmente senza alterarne la larghezza. +**File**: `src/embed.ts` — handler del bottone collapse in `createPortalPanel()`. + +--- + +#### Bug 7 — Pannello portale rimane nel DOM dopo disabilitazione plugin — RISOLTO ✅ + +**Sintomo**: disabilitando il plugin, i pannelli portale (bottoni) rimanevano visibili nel documento. +**Fix**: `plugin.register(() => panel.remove())` — Obsidian chiama tutti i callback registrati con `plugin.register()` quando il plugin viene disabilitato. +**File**: `src/embed.ts` — `createPortalPanel()`. + +--- + +#### Bug 8 — Bottoni pannello portale non cliccabili — RISOLTO ✅ + +**Sintomo**: dopo lo spostamento del pannello dentro lo span, i bottoni non rispondevano al click. +**Causa**: lo span aveva `pointer-events: none` (impostato in `tryDecorate()` per handwriting Android). Il pannello figlio ereditava la proprietà. +**Fix**: `pointer-events: auto` in CSS su `.hwm_portal-panel` — ripristina i click solo sul pannello, lasciando il resto dello span non interattivo. +**File**: `styles.css`. + +--- + +#### Bug 9 — Palette colori SVG non aggiornata al cambio bgMode — RISOLTO ✅ + +**Sintomo**: cambiando bgMode nelle impostazioni, gli SVG nei documenti aperti mantenevano i vecchi colori dei tratti finché non si ricaricava il plugin. +**Fix**: aggiunto listener `onBgModeRemap` in `registerEmbed()` registrato in `plugin.bgModeListeners`. Quando il bgMode cambia: +1. Itera `plugin.embedPaths` (mappa `embedId → svgPath`) +2. Legge il file SVG dal vault e verifica il marker `hwm-strokes` (ignora SVG non del plugin) +3. Parsa `viewBox="0 0 W H"` per ottenere le dimensioni reali (non quelle di default, che perderebbero l'auto-expand) +4. Rimappa i colori dei tratti con `remapStrokeColor()`, rigenera l'SVG con `strokesToSvg()`, salva +5. Chiama `plugin.refreshPreview(embedId, newContent)` → aggiorna `img.src` con cache-bust +**File**: `src/embed.ts` — `onBgModeRemap` in `registerEmbed()`. + +--- + +#### Bug 10 — Toolbar editor non aggiornata al cambio bgMode — RISOLTO ✅ + +**Sintomo**: i bottoni della toolbar nel `DrawingModal` (Windows) e nel `DrawingEditorView` (Android) non cambiavano colore al cambio bgMode. +**Causa 1 (live update)**: mancava un listener. Aggiunto `bgModeListener` in entrambe le classi, registrato in `bgModeListeners` dopo la costruzione di `colorBtns` (per poterli aggiornare nel closure). Rimosso in `onClose()`. +**Causa 2 (riapertura)**: il cambio bgMode avviene sempre con editor chiuso. `buildEditor()` viene chiamato di nuovo alla riapertura e legge `plugin.settings.bgMode` → corretto per costruzione. Il problema visivo era CSS. +**Causa 3 (CSS)**: Obsidian dark theme ha regole tipo `.modal-content button { background: var(...) }` con specificità `0,1,1` > `0,1,0` di `.hwm_btn`, sovrascrivendo il nostro sfondo trasparente. Il colore del topbar senza `!important` veniva sovrascritto analogamente. +**Fix CSS** (`styles.css`): +- `.hwm_editor-topbar { background: rgba(240,240,240,0.95) !important }` e `.hwm_editor-topbar--dark { background: rgba(40,40,40,0.97) !important }` — entrambe con `!important` (la `--dark` appare dopo → vince in dark mode per cascade order) +- `.hwm_editor-topbar .hwm_btn { background: transparent !important; color: #333 !important }` — batte la specificità di Obsidian +- `.hwm_editor-topbar--dark .hwm_btn { color: #bbb !important }` — appare dopo → vince in dark mode +- Hover e active espliciti con `!important` per entrambe le modalità +**Nota importante**: il cambio bgMode viene SEMPRE effettuato con editor chiuso (il modal copre l'intera finestra). Il listener live è presente ma non è il percorso principale. +**File**: `src/editor-view.ts` + `styles.css`. + +--- + ### Altri task aperti 1. **Migliorare il riconoscimento dei caratteri speciali markdown** — `src/recognizer.ts` + `src/md-parser.ts`: @@ -270,6 +342,33 @@ Più: `img { display: none !important }` per nascondere l'SVG e `::after { conte - Obiettivo: unico codice toolbar condiviso, compatta di default su entrambe le piattaforme - File coinvolti: `src/editor-view.ts` + `styles.css` +4. **Migliorare il riconoscimento OCR da Gemini** — `src/recognizer.ts` + `src/md-parser.ts`: + - Migliorare la qualità complessiva del riconoscimento, non solo i simboli markdown + - Affinare il prompt con few-shot examples per simboli (`#`, `-`, `>`, `**`, `==`, ecc.) + - Valutare se `md-parser.ts` può coprire i casi che il prompt non gestisce + - Valutare modelli Gemini alternativi o parametri diversi (temperatura, ecc.) + +5. **Tema automatico — seguire il tema di Obsidian o di sistema** — `src/settings.ts` + `src/embed.ts` + `src/editor-view.ts`: + - Attualmente il bgMode (chiaro/scuro/custom) è impostato manualmente nelle Settings + - Obiettivo: opzione "Automatico" che legge il tema corrente di Obsidian e si adatta + - **Alternativa 1 (più semplice)**: leggere `document.body.classList.contains('theme-dark')` → se true, bgMode = dark; altrimenti light. Obsidian aggiunge/rimuove `theme-dark` dal body al cambio tema. + - **Alternativa 2**: `window.matchMedia('(prefers-color-scheme: dark)').matches` → segue il tema di sistema OS (indipendente da Obsidian) + - L'alternativa 1 è preferibile perché l'utente potrebbe usare un tema Obsidian diverso da quello di sistema + - Aggiungere un `MutationObserver` su `document.body` per rilevare il cambio della classe `theme-dark` e chiamare `notifyBgModeChange()` automaticamente + +6. **Angoli arrotondati al riquadro e all'overlay** — `styles.css`: + - Riquadro inline (lo span `.internal-embed`): aggiungere `border-radius` all'immagine o al container + - Overlay `DrawingModal` (`.hwm_modal`): aggiungere `border-radius` al modal e al canvas interno + - Attenzione: l'img dentro lo span non rispetta `border-radius` dello span senza `overflow: hidden` sul container + +7. **Rimuovere lo switch "Modalità handwriting Android"** — `src/settings.ts` + `src/embed.ts` + `src/main.ts` + `styles.css`: + - Lo switch `hwmHandwritingMode` era un workaround per il bug handwriting Android (BUG APERTO), ora considerato comportamento definitivo + - Rendere la badge mode **sempre attiva su Android** e la preview piena **sempre attiva su Desktop**, senza toggle manuale + - Rimuovere `hwmHandwritingMode` da `HandwritingSettings` e `DEFAULT_SETTINGS` in `settings.ts` + - Rimuovere la voce dalla pagina impostazioni + - In `registerEmbed()`: sostituire la lettura di `hwmHandwritingMode` con `Platform.isMobile` + - Mantenere le classi CSS `hwm-handwriting-mode` e `hwm-badge-mode` (solo applicarle automaticamente) + ### Problemi risolti - **Handwriting Android (disegno)** ✅ — risolto con editor in tab separata (`ItemView`), canvas fuori da `cm-content` @@ -292,6 +391,12 @@ Più: `img { display: none !important }` per nascondere l'SVG e `::after { conte - **Canvas modal non centrato, toolbar a sinistra (Bug 2)** ✅ — `display: flex; justify-content: center` su `.hwm_canvas-wrap`; `max-width: 100%` su `.hwm_canvas`; classe `hwm_editor-topbar--modal` aggiunta al topbar del modal per centrare la toolbar - **Auto-scroll sposta tratti durante disegno (Bug 3)** ✅ — aggiunto `isPointerDown()` in `DrawingCanvas`; scroll automatico bloccato se il pointer è premuto, sia in `DrawingEditorView` che in `DrawingModal` - **Badge mode mostra riquadro minuscolo (Bug 4)** ✅ — CSS `.hwm-handwriting-mode .hwm-badge-mode` con `width: 100% !important`, `height: 72px`, flex centrato, `img { display: none }` + `::after` con emoji matita +- **Pannello portale visibile nelle impostazioni (Bug 5)** ✅ — pannello spostato da `document.body` (position:fixed) a figlio diretto dello span (position:absolute); eliminati RAF loop e scroll listener +- **Comprimi/Espandi modificava larghezza (Bug 6)** ✅ — usa `container.style.height + overflow:hidden` invece di `max-height` sull'img +- **Pannello resta dopo disabilitazione plugin (Bug 7)** ✅ — `plugin.register(() => panel.remove())` +- **Bottoni pannello non cliccabili (Bug 8)** ✅ — `pointer-events: auto` su `.hwm_portal-panel` in CSS +- **SVG non aggiornati al cambio bgMode (Bug 9)** ✅ — listener `onBgModeRemap` in `bgModeListeners`; legge viewBox per dimensioni reali, rimappa colori, salva SVG, refresh preview +- **Toolbar editor non aggiornata al cambio bgMode (Bug 10)** ✅ — `bgModeListener` in `DrawingEditorView` e `DrawingModal`; CSS `!important` su topbar e bottoni per battere specificità Obsidian dark theme ### BUG APERTO — Handwriting disabilitato nel documento quando il riquadro è presente diff --git a/HandTranscriptMd/src/editor-view.ts b/HandTranscriptMd/src/editor-view.ts index 7a56c78..d9cc606 100644 --- a/HandTranscriptMd/src/editor-view.ts +++ b/HandTranscriptMd/src/editor-view.ts @@ -37,6 +37,8 @@ export class DrawingEditorView extends ItemView { private svgPath = ''; private sourcePath = ''; private saveTimer: ReturnType | null = null; + // Listener per aggiornare la classe dark al cambio bgMode + private bgModeListener: ((bgMode: string) => void) | null = null; constructor(leaf: WorkspaceLeaf, plugin: HandwritingPlugin) { super(leaf); @@ -70,6 +72,11 @@ export class DrawingEditorView extends ItemView { 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; + } } /* ---------- Costruisce la UI dell'editor ---------- */ @@ -149,6 +156,31 @@ export class DrawingEditorView extends ItemView { }; 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']; + const darkColors = ['#ffffff', '#60a5fa', '#f87171', '#4ade80']; + this.bgModeListener = (bgMode: string) => { + const dark = bgMode === 'dark'; + topbar.classList.toggle('hwm_editor-topbar--dark', dark); + toolbar.classList.toggle('hwm_toolbar--dark', dark); + el.style.backgroundColor = getEffectiveBgColor(this.plugin.settings); + // Aggiorna i pallini colore palette (backgroundColor inline con !important) + const newColors = dark ? darkColors : lightColors; + colorBtns.forEach((btn, i) => { + btn.style.backgroundColor = newColors[i] ?? ''; + btn.setAttribute('title', newColors[i] ?? ''); + }); + // Aggiorna sfondo e righe nel canvas + if (this.canvas) { + this.canvas.setBackground( + getEffectiveBgColor(this.plugin.settings), + getEffectiveLineColor(this.plugin.settings) + ); + } + }; + this.plugin.bgModeListeners.add(this.bgModeListener); + // Undo / Redo / Clear const undoBtn = this.mkBtn(toolbar, 'rotate-ccw', 'Annulla'); undoBtn.classList.add('hwm_undo-btn'); @@ -430,6 +462,8 @@ export class DrawingModal extends Modal { private sourcePath: string; private canvas: DrawingCanvas | null = null; private saveTimer: ReturnType | null = null; + // Listener per aggiornare la classe dark al cambio bgMode + private bgModeListener: ((bgMode: string) => void) | null = null; // Callback invocato alla chiusura del modal (usato per nascondere/mostrare il bottone matita) onClosed?: () => void; @@ -454,6 +488,11 @@ export class DrawingModal extends Modal { 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?.(); } @@ -475,6 +514,7 @@ export class DrawingModal extends Modal { 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'); @@ -518,6 +558,30 @@ export class DrawingModal extends Modal { }; 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']; + this.bgModeListener = (bgMode: string) => { + const dark = bgMode === 'dark'; + topbar.classList.toggle('hwm_editor-topbar--dark', dark); + toolbar.classList.toggle('hwm_toolbar--dark', dark); + el.style.backgroundColor = getEffectiveBgColor(this.plugin.settings); + // Aggiorna i pallini colore palette + const newColors = dark ? darkColors : lightColors; + colorBtns.forEach((btn, i) => { + btn.style.backgroundColor = newColors[i] ?? ''; + btn.setAttribute('title', newColors[i] ?? ''); + }); + // Aggiorna sfondo e righe nel canvas + if (this.canvas) { + this.canvas.setBackground( + getEffectiveBgColor(this.plugin.settings), + getEffectiveLineColor(this.plugin.settings) + ); + } + }; + this.plugin.bgModeListeners.add(this.bgModeListener); + const undoBtn = this.mkBtn(toolbar, 'rotate-ccw', 'Annulla'); undoBtn.classList.add('hwm_undo-btn'); const redoBtn = this.mkBtn(toolbar, 'rotate-cw', 'Ripristina'); diff --git a/HandTranscriptMd/src/embed.ts b/HandTranscriptMd/src/embed.ts index e15769e..3f50d67 100644 --- a/HandTranscriptMd/src/embed.ts +++ b/HandTranscriptMd/src/embed.ts @@ -28,7 +28,7 @@ const ICONS: Record = { import type HandwritingPlugin from './main'; import { Stroke } from './drawing-canvas'; import { strokesToSvg, parseSvgStrokes, generateId } from './svg-utils'; -import { getEffectiveBgColor, getEffectiveLineColor, remapStrokeColor } from './settings'; +import { getEffectiveBgColor, getEffectiveLineColor, remapStrokeColor, BgMode } from './settings'; import { getRecognizer } from './recognizer'; import { parseMarkdown } from './md-parser'; import { VIEW_TYPE_HANDWRITING, DrawingEditorView, DrawingModal } from './editor-view'; @@ -47,6 +47,43 @@ export function registerEmbed(plugin: HandwritingPlugin) { // Applica/rimuove la classe CSS badge mode all'avvio in base alle impostazioni document.body.classList.toggle('hwm-handwriting-mode', plugin.settings.hwmHandwritingMode); + // --- Listener globale: remap automatico colori SVG al cambio bgMode --- + // Quando l'utente cambia sfondo canvas nelle impostazioni, tutti gli SVG del plugin + // attualmente tracciati in embedPaths vengono letti, rimappati e risalvati nel vault. + // Identifica gli SVG del plugin tramite — gli SVG + // dell'utente non hanno questo tag e vengono ignorati. + const onBgModeRemap = async (bgMode: string) => { + for (const [embedId, svgPath] of plugin.embedPaths) { + const file = plugin.app.vault.getAbstractFileByPath(svgPath); + if (!(file instanceof TFile)) continue; + const content = await plugin.app.vault.read(file); + // Salta SVG non creati dal plugin (assenza del tag hwm-strokes) + if (!content.includes('hwm-strokes')) continue; + const strokes = parseSvgStrokes(content); + // Rimappa i colori dei tratti al nuovo tema + const remapped = strokes.map(s => ({ + ...s, color: remapStrokeColor(s.color, bgMode as BgMode) + })); + // Legge dimensioni reali dal viewBox per preservare l'altezza raggiunta con auto-expand. + // Usare plugin.settings.canvasHeight causerebbe il "collasso" degli SVG cresciuti. + const dimMatch = content.match(/viewBox="0 0 (\d+) (\d+)"/); + const svgWidth = dimMatch ? parseInt(dimMatch[1]!) : plugin.settings.canvasWidth; + const svgHeight = dimMatch ? parseInt(dimMatch[2]!) : plugin.settings.canvasHeight; + const newSvg = strokesToSvg( + remapped, + svgWidth, + svgHeight, + getEffectiveBgColor(plugin.settings), + getEffectiveLineColor(plugin.settings) + ); + await plugin.app.vault.modify(file, newSvg); + // Aggiorna l' nella preview inline con cache-bust + plugin.refreshPreview(embedId, newSvg); + } + }; + plugin.bgModeListeners.add(onBgModeRemap); + plugin.register(() => plugin.bgModeListeners.delete(onBgModeRemap)); + // --- NUOVO: MutationObserver su document.body --- // Intercetta gli span .internal-embed con src _handwriting/ non appena // appaiono nel DOM — funziona sia in reading view che in live preview @@ -570,26 +607,37 @@ function createPortalPanel( const isDark = plugin.settings.bgMode === 'dark'; const collapsedHeight = plugin.settings.canvasHeight; let isExpanded = true; - // Flag per nascondere il bottone matita quando il modal (Desktop) è aperto + // Flag per nascondere il pannello quando il modal (Desktop) è aperto let modalOpen = false; - // Pannello floating con sfondo e ombra, posizionato via position: fixed + // Lo span diventa position: relative per ancorarvi il pannello (position: absolute). + container.style.position = 'relative'; + const panel = document.createElement('div'); panel.className = 'hwm_portal-panel'; if (isDark) panel.classList.add('hwm_toolbar--dark'); - document.body.appendChild(panel); + container.appendChild(panel); + // Rimuove il pannello dal DOM quando il plugin viene disabilitato + plugin.register(() => panel.remove()); + + // Traccia embedId → svgPath per il remap automatico al cambio bgMode + plugin.embedPaths.set(embedId, svgPath); // --- Bottone matita --- // Desktop: apre DrawingModal (overlay fullscreen, senza aprire nuova tab) - // Mobile: apre DrawingEditorView in una nuova tab (fuori da cm-content → nessun conflitto handwriting) + // Mobile: apre DrawingEditorView in una nuova tab const pencilBtn = createPanelBtn(panel, 'pencil', 'Apri editor disegno'); pencilBtn.addEventListener('click', async () => { if (Platform.isDesktop) { - // Modal overlay — rimane nella stessa finestra if (modalOpen) return; modalOpen = true; + // Nasconde il pannello mentre il modal è aperto (altrimenti galleggerebbe sul canvas) + panel.style.display = 'none'; const modal = new DrawingModal(plugin.app, plugin, embedId, svgPath, sourcePath); - modal.onClosed = () => { modalOpen = false; }; + modal.onClosed = () => { + modalOpen = false; + if (container.isConnected) panel.style.display = ''; + }; modal.open(); } else { // Mobile: nuova tab (DrawingEditorView è fuori da cm-content) @@ -626,18 +674,22 @@ function createPortalPanel( }); // --- 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'); collapseBtn.classList.add('hwm_collapse-btn'); collapseBtn.addEventListener('click', () => { isExpanded = !isExpanded; if (isExpanded) { - container.style.maxHeight = ''; - container.style.overflow = ''; + container.style.height = ''; + container.style.overflow = ''; collapseBtn.classList.remove('hwm_rotated'); collapseBtn.title = 'Comprimi'; } else { - container.style.maxHeight = collapsedHeight + 'px'; - container.style.overflow = 'hidden'; + container.style.height = collapsedHeight + 'px'; + container.style.overflow = 'hidden'; collapseBtn.classList.add('hwm_rotated'); collapseBtn.title = 'Espandi'; } @@ -651,53 +703,32 @@ function createPortalPanel( await removeWikiEmbed(svgPath, sourcePath, plugin); }); - // Scroll fix: nasconde il pannello immediatamente durante lo scroll - // per evitare che rimanga in posizione sbagliata tra un frame RAF e l'altro. - // Il RAF lo riposiziona e lo rende visibile di nuovo appena la pagina si ferma. - let scrollTimer: ReturnType | null = null; - const onScroll = () => { - panel.style.visibility = 'hidden'; - if (scrollTimer) clearTimeout(scrollTimer); - scrollTimer = setTimeout(() => { panel.style.visibility = ''; }, 150); - }; - // Ascolta sia il .cm-scroller (live preview) che .markdown-reading-view (reading view) - const scrollEl = container.closest('.cm-scroller') as HTMLElement | null - ?? container.closest('.markdown-reading-view') as HTMLElement | null; - if (scrollEl) { - scrollEl.addEventListener('scroll', onScroll, { passive: true }); - plugin.register(() => scrollEl.removeEventListener('scroll', onScroll)); - } - - // RAF loop: aggiorna posizione e visibilità ogni frame. - // Si auto-ferma quando il container lascia il DOM. - const update = () => { + // Layout-change: su Mobile nasconde il pannello quando la tab editor è aperta + const onLayoutChange = () => { if (!container.isConnected) { - panel.remove(); + plugin.app.workspace.off('layout-change', onLayoutChange); + plugin.embedPaths.delete(embedId); return; } - const rect = container.getBoundingClientRect(); - const inViewport = rect.width > 0 && rect.top < window.innerHeight && rect.bottom > 0; - // Controlla se la tab editor (Mobile) è aperta per questo embed - const tabOpen = plugin.app.workspace.getLeavesOfType(VIEW_TYPE_HANDWRITING) - .some(l => (l.view as DrawingEditorView).getEmbedId() === embedId); - - // Nasconde l'INTERO pannello se: editor aperto (modal su Desktop o tab su Mobile) - // oppure il container non è nel viewport. - // Quando il modal è aperto a schermo intero, il pannello rimarrebbe sopra di esso: - // nasconderlo evita che i bottoni siano cliccabili sopra il canvas di disegno. - if (!inViewport || modalOpen || tabOpen) { - panel.style.display = 'none'; - } else { - panel.style.display = 'flex'; - // Posizionato in alto a destra del container via position: fixed. - // "right" con position:fixed = distanza dal bordo destro del viewport. - panel.style.top = (rect.top + 6) + 'px'; - panel.style.right = (window.innerWidth - rect.right + 6) + 'px'; - panel.style.left = 'auto'; + if (Platform.isMobile) { + const tabOpen = plugin.app.workspace.getLeavesOfType(VIEW_TYPE_HANDWRITING) + .some(l => (l.view as DrawingEditorView).getEmbedId() === embedId); + panel.style.display = tabOpen ? 'none' : ''; } - requestAnimationFrame(update); }; - requestAnimationFrame(update); + plugin.app.workspace.on('layout-change', onLayoutChange); + plugin.register(() => plugin.app.workspace.off('layout-change', onLayoutChange)); + + // BgMode listener: aggiorna la classe dark sul pannello al cambio tema + const onBgMode = (bgMode: string) => { + if (!container.isConnected) { + plugin.bgModeListeners.delete(onBgMode); + return; + } + panel.classList.toggle('hwm_toolbar--dark', bgMode === 'dark'); + }; + plugin.bgModeListeners.add(onBgMode); + plugin.register(() => plugin.bgModeListeners.delete(onBgMode)); } // Crea un bottone div nel pannello portale diff --git a/HandTranscriptMd/src/main.ts b/HandTranscriptMd/src/main.ts index 3da5da3..c6030dc 100644 --- a/HandTranscriptMd/src/main.ts +++ b/HandTranscriptMd/src/main.ts @@ -18,11 +18,23 @@ export default class HandwritingPlugin extends Plugin { // Mappa di callback per aggiornare le preview inline quando l'editor tab salva public previewCallbacks = new Map void>(); + // Mappa embedId → svgPath: permette di trovare i file SVG da rimappare al cambio bgMode + public embedPaths = new Map(); + + // Callback notificate quando l'utente cambia bgMode nelle impostazioni + public bgModeListeners = new Set<(bgMode: string) => void>(); + // Invocato dall'editor tab dopo ogni salvataggio per aggiornare la preview inline refreshPreview(id: string, svgContent: string) { this.previewCallbacks.get(id)?.(svgContent); } + // Chiamato da settings quando l'utente cambia bgMode: + // notifica pannelli (aggiornamento classe dark) e SVG attivi (remap colori) + notifyBgModeChange() { + this.bgModeListeners.forEach(cb => cb(this.settings.bgMode)); + } + async onload() { await this.loadSettings(); diff --git a/HandTranscriptMd/src/settings.ts b/HandTranscriptMd/src/settings.ts index 4aeacb7..3891e68 100644 --- a/HandTranscriptMd/src/settings.ts +++ b/HandTranscriptMd/src/settings.ts @@ -171,6 +171,8 @@ export class HandwritingSettingTab extends PluginSettingTab { .onChange(async (value) => { this.plugin.settings.bgMode = value as BgMode; await this.plugin.saveSettings(); + // Notifica pannelli (dark class) e SVG attivi (remap colori) + this.plugin.notifyBgModeChange(); // Aggiorna la UI per mostrare/nascondere il color picker this.display(); })); diff --git a/HandTranscriptMd/styles.css b/HandTranscriptMd/styles.css index 79e2797..fa109e0 100644 --- a/HandTranscriptMd/styles.css +++ b/HandTranscriptMd/styles.css @@ -22,7 +22,7 @@ handwriting mostrano un box compatto con icona invece dell'SVG a piena altezza. Questo riduce l'area CE=false che interferisce con la proximity detection dello stylus handwriting di Android. - L'SVG viene nascosto; il pannello portale (position:fixed) rimane visibile + L'SVG viene nascosto; il pannello portale (position:absolute) rimane visibile sopra il box per permettere apertura editor, conversione ed eliminazione. */ .hwm-handwriting-mode .hwm-badge-mode { /* Larghezza piena: lo span .internal-embed di default può restare stretto @@ -406,10 +406,15 @@ cm-content → non rompe handwriting Android ======================================== */ -/* Pannello floating con tutti i bottoni (nuovo formato ![[svg]]) */ +/* Pannello con tutti i bottoni (nuovo formato ![[svg]]). + Vive dentro lo span .internal-embed (position: relative impostato via JS). + Con position: absolute segue il DOM: scompare quando lo span non è visibile + (scroll, impostazioni aperte, cambio tab) senza nessuna logica JS aggiuntiva. */ .hwm_portal-panel { - position: fixed; /* coordinate viewport, aggiornate via RAF */ - z-index: 100; + position: absolute; + top: 6px; + right: 6px; + z-index: 10; /* sufficiente per stare sopra l'img interna */ display: flex; align-items: center; gap: 0; /* spacing via margin-right sui .hwm_btn */ @@ -417,6 +422,9 @@ border-radius: 6px; background: rgba(240, 240, 240, 0.95); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15); + /* Lo span padre ha pointer-events:none (per handwriting Android): + ripristina i click sui bottoni del pannello */ + pointer-events: auto; } /* Bottone cerchio singolo (formato legacy) */ @@ -489,14 +497,43 @@ align-items: center; justify-content: space-between; padding: 6px 8px; - background: rgba(240, 240, 240, 0.95); - border-bottom: 1px solid #ddd; + background: rgba(240, 240, 240, 0.95) !important; /* !important: impedisce a Obsidian dark mode di sovrascrivere il topbar */ + border-bottom: 1px solid #ddd !important; flex-shrink: 0; gap: 8px; } .hwm_editor-topbar--dark { - background: rgba(40, 40, 40, 0.97); - border-bottom-color: #444; + background: rgba(40, 40, 40, 0.97) !important; + border-bottom-color: #444 !important; +} + +/* Forza background e colore icone dentro il topbar dell'editor. + Obsidian dark theme ha regole tipo ".modal-content button { background: var(...) }" + con specificità 0,1,1 > 0,1,0 di .hwm_btn: senza !important qui sotto, i bottoni + rimangono scuri anche in light mode. Le due regole .hwm_btn e --dark .hwm_btn hanno + stessa specificità (0,2,0) ma --dark appare dopo → vince in dark mode. */ +.hwm_editor-topbar .hwm_btn { + background: transparent !important; + color: #333 !important; +} +.hwm_editor-topbar--dark .hwm_btn { + background: transparent !important; + color: #bbb !important; +} +/* Hover light: sfondo grigio chiaro */ +.hwm_editor-topbar .hwm_btn:hover:not(.hwm_active) { + background: #e0e0e0 !important; + color: #333 !important; +} +/* Hover dark: sfondo bianco semi-trasparente */ +.hwm_editor-topbar--dark .hwm_btn:hover:not(.hwm_active) { + background: rgba(255, 255, 255, 0.12) !important; + color: #fff !important; +} +/* Bottone attivo (tool selezionato): sempre sfondo accent e icona bianca */ +.hwm_editor-topbar .hwm_btn.hwm_active { + background: var(--interactive-accent) !important; + color: #ffffff !important; } /* Bottone ← (indietro, a sinistra) */