mirror of
https://github.com/gabriele-cusato/HandTranscriptMd.git
synced 2026-07-22 14:30:25 +00:00
accettato il limite dell'handwriting, corretti vari bug
This commit is contained in:
parent
3a906bc4c7
commit
315e5f597e
6 changed files with 318 additions and 67 deletions
117
CLAUDE.md
117
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 `<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
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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) */
|
||||
|
|
|
|||
Loading…
Reference in a new issue