accettato il limite dell'handwriting, corretti vari bug

This commit is contained in:
gabriele-cusato 2026-03-23 02:03:11 +01:00
parent 3a906bc4c7
commit 315e5f597e
6 changed files with 318 additions and 67 deletions

117
CLAUDE.md
View file

@ -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 `<desc>` 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 `<img>`, 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

View file

@ -37,6 +37,8 @@ export class DrawingEditorView extends ItemView {
private svgPath = '';
private sourcePath = '';
private saveTimer: ReturnType<typeof setTimeout> | null = null;
// Listener per aggiornare la classe dark al cambio bgMode
private bgModeListener: ((bgMode: string) => void) | null = null;
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<typeof setTimeout> | null = null;
// Listener per aggiornare la classe dark al cambio bgMode
private bgModeListener: ((bgMode: string) => void) | null = null;
// Callback invocato alla chiusura del modal (usato per nascondere/mostrare il bottone matita)
onClosed?: () => void;
@ -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');

View file

@ -28,7 +28,7 @@ const ICONS: Record<string, string> = {
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 <desc class="hwm-strokes"> — 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'<img> 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'<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');
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<typeof setTimeout> | 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

View file

@ -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<string, (svgContent: string) => void>();
// Mappa embedId → svgPath: permette di trovare i file SVG da rimappare al cambio bgMode
public embedPaths = new Map<string, string>();
// 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();

View file

@ -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();
}));

View file

@ -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) */