mirror of
https://github.com/gabriele-cusato/HandTranscriptMd.git
synced 2026-07-22 14:30:25 +00:00
verificato limite architetturale, anche con svg, se scrivo nel canvas nel documento si attiva la modalità drawing e non viene piu preso l'handwriting. Ormai le strade da provare su questo sono poche. Si potrebbe mettere uno switch, ovvero se io voglio handwriting, i riquadri non si vedono ma se ci clicco poi si apre la nuova scheda, altrimenti se non mi interessa posso avere la preview direttamente nel documento.
This commit is contained in:
parent
d543fcdb97
commit
d51f8f54ba
12 changed files with 2140 additions and 544 deletions
|
|
@ -6,7 +6,8 @@
|
|||
"WebFetch(domain:groups.google.com)",
|
||||
"WebFetch(domain:css-tricks.com)",
|
||||
"WebFetch(domain:chromium.googlesource.com)",
|
||||
"WebFetch(domain:github.com)"
|
||||
"WebFetch(domain:github.com)",
|
||||
"WebFetch(domain:developer.chrome.com)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
269
CLAUDE.md
269
CLAUDE.md
|
|
@ -40,92 +40,126 @@ Un plugin Obsidian che inserisce un **riquadro canvas inline** in un file `.md`
|
|||
|
||||
---
|
||||
|
||||
## Stato attuale — Fase 2 COMPLETATA, Fase 3 IN CORSO (fix Android handwriting)
|
||||
## Stato attuale — Fase 3 COMPLETATA (editor in tab separata)
|
||||
|
||||
### File del plugin (`HandTranscriptMd/src/`)
|
||||
|
||||
| File | Cosa fa |
|
||||
|------|---------|
|
||||
| `main.ts` | Entry point: registra embed, comando "Insert handwriting block", ribbon icon, settings |
|
||||
| `main.ts` | Entry point: registra embed, editor view, comando, ribbon, settings, `previewCallbacks` per sync inline↔tab |
|
||||
| `settings.ts` | Impostazioni: cartella SVG, dimensioni canvas, sfondo, lingue OCR, chiave API Gemini (campo password) |
|
||||
| `drawing-canvas.ts` | Motore disegno Canvas API: Bézier quadratiche, penna, gomma parziale, undo/redo history-based, auto-expand, righe foglio |
|
||||
| `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, toolbar, bottone Converti collegato (OCR → markdown → sostituisce code block), `svgToBase64Png()` helper |
|
||||
| `embed.ts` | Code block processor → preview SVG inline con 3 bottoni (X, converti, comprimi) in alto a sinistra + bottone portale in document.body; preview non cliccabile |
|
||||
| `editor-view.ts` | `DrawingEditorView extends ItemView` — editor canvas in tab Obsidian dedicata, toolbar completa, scroll, 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 |
|
||||
|
||||
### Architettura attuale (Fase 3+)
|
||||
|
||||
**Due formati supportati:**
|
||||
- **NUOVO (default)**: `![[_handwriting/hw_xxx.svg]]` — SVG visibile nativamente anche senza plugin
|
||||
- **LEGACY**: `` ```handwriting {"id":..., "svg":...}``` `` — vecchio formato, mantenuto per compatibilità
|
||||
|
||||
**Preview inline — Nuovo formato wiki:**
|
||||
- Obsidian renderizza `![[svg]]` come `<span class="internal-embed image-embed">[img][/img]</span>`
|
||||
- **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)
|
||||
- **ZERO modifica al DOM dello span** — nessuna classe aggiunta, nessun figlio inserito, nessuno stile inline. Lo span resta identico a qualsiasi altra immagine Obsidian
|
||||
- **Pannello portale** (`hwm_portal-panel`) in `document.body` con 4 bottoni: ✏️ (apre tab editor), 📄 (converti OCR), ↕️ (comprimi/espandi), ✕ (elimina). Posizionato via RAF + `getBoundingClientRect()` + `position: fixed`
|
||||
- **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
|
||||
|
||||
**Preview inline — Formato legacy:**
|
||||
- Mostra l'SVG come CSS `background-image` su un `<div>` (no `<img>`)
|
||||
- 3 bottoni inline (`<div role="button">`, non `<button>`) dentro il container del code block
|
||||
- Bottone portale singolo (`hwm_portal-btn`, cerchio matita) in `document.body` per aprire la tab editor
|
||||
|
||||
**Editor in tab separata (`editor-view.ts`):**
|
||||
- `DrawingEditorView extends ItemView` di Obsidian
|
||||
- Canvas in un DOM completamente separato da CodeMirror → **nessun conflitto handwriting Android**
|
||||
- Top bar: bottone ← a sinistra (chiude tab), toolbar completa a destra
|
||||
- Scroll container: `overflow-y: auto` per canvas più grandi dello schermo
|
||||
- Auto-scroll: callback `onResize` su DrawingCanvas → scrolla verso il basso durante auto-expand
|
||||
- Finger scroll: `allowFingerScroll(scrollContainer)` — `touch-action` resta `none` (penna non scrolla), dito scrolla il container via `setPointerCapture` + JS manuale
|
||||
- Resize handle visibile ma `pointer-events: none` (no drag manuale, solo auto-expand)
|
||||
- Auto-save debounced 2s + callback `plugin.refreshPreview()` aggiorna la preview inline
|
||||
- Converti/Elimina funzionano anche dalla tab (manipolano il .md via `sourcePath`)
|
||||
|
||||
### Funzionalità implementate
|
||||
|
||||
- **Canvas inline** nel markdown via code block `handwriting`
|
||||
- **Disegno diretto** — click sul blocco = disegno immediato (no click "Edit")
|
||||
- **Preview SVG inline** nel markdown via code block `handwriting` (immagine statica, no canvas)
|
||||
- **Editor in tab dedicata** — click sulla preview apre una tab Obsidian separata
|
||||
- **Curve smooth** — Bézier quadratiche con tecnica midpoint
|
||||
- **Gomma parziale** — cancella solo i punti toccati, taglia i tratti in segmenti
|
||||
- **Undo/Redo** — basato su history di stati (funziona sia per disegno che per gomma)
|
||||
- **Auto-expand** — il canvas si espande con animazione smooth (requestAnimationFrame, ease-out cubico)
|
||||
- **Resize manuale** — handle trascinabile in basso
|
||||
- **Auto-expand** — il canvas si espande con animazione smooth + auto-scroll nel container
|
||||
- **Clear → reset** alla dimensione di default con animazione
|
||||
- **Righe orizzontali** — foglio a righe (32px), sia nel canvas che nell'SVG
|
||||
- **Temi sfondo** — chiaro/scuro/custom con color picker nelle impostazioni
|
||||
- **Remapping colori automatico** — i tratti si adattano al cambio tema (nero↔bianco, blu↔azzurro, ecc.)
|
||||
- **Toolbar completa** — penna, gomma, 4 colori, undo, redo, clear, converti, salva, elimina (X)
|
||||
- **Elimina riquadro** — bottone X rimuove code block dal .md e cancella SVG
|
||||
- **Auto-save** — salvataggio debounced 2s dopo l'ultima modifica
|
||||
- **Elimina riquadro** — da inline (3 bottoni) o da tab editor
|
||||
- **Auto-save** — salvataggio debounced 2s + refresh preview inline
|
||||
- **SVG standard** — file `.svg` nella cartella `_handwriting/`, visibili da qualsiasi dispositivo
|
||||
- **Palette colori adattiva** — colori scuri su sfondo chiaro, colori chiari su sfondo scuro
|
||||
- **Resize handle tematizzato** — si adatta al tema scuro
|
||||
- **OCR via Gemini** — bottone Converti: SVG → PNG base64 → Gemini 3.1 Flash Lite → testo → `md-parser` → sostituisce code block nel `.md`
|
||||
- **Archiviazione SVG** — dopo la conversione, il file SVG viene spostato in `_handwriting/_converted/AAAA-MM-GG_HH-MM-SS.svg`
|
||||
- **OCR via Gemini** — SVG → PNG base64 → Gemini 3.1 Flash Lite → `md-parser` → sostituisce code block
|
||||
- **Archiviazione SVG** — dopo la conversione, SVG spostato in `_handwriting/_converted/AAAA-MM-GG_HH-MM-SS.svg`
|
||||
- **Settings OCR** — chiave API Gemini (campo password) + lingue OCR configurabili (default: `it, en`)
|
||||
- **Supporto Android** — comportamenti differenziati tra Windows e Android:
|
||||
- Toolbar compatta su mobile (▼/▲ toggle), espansa su Windows
|
||||
- Icone SVG inline (mappa `ICONS` in `embed.ts`) — identiche su Windows e Android, nessuna dipendenza da `setIcon`/Lucide bundled — **RISOLTO**
|
||||
- Disegno solo con penna (`pointerType === 'pen'`), dito ignorato su mobile per il disegno
|
||||
- Focus fix multi-livello: `inputmode="none"` + listener `beforeinput` in capture phase + blur `.cm-editor` + `canvas.focus()` su pointerdown penna
|
||||
- Cerchi colori: bottoni colore sono `<div>` (non `<button>`) + dimensioni forzate via `style.setProperty(..., 'important')` — **RISOLTO**
|
||||
- **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** — pulsante cerchio con icona matita in `document.body` (fuori da cm-content), si posiziona sopra il riquadro via RAF + getBoundingClientRect, si nasconde automaticamente se la tab editor è già aperta o il riquadro esce dal viewport
|
||||
- **Preview non tappabile** — click handler rimosso; l'unico modo per aprire l'editor è il bottone portale
|
||||
|
||||
### Embedding nel markdown
|
||||
|
||||
**Nuovo formato (default):**
|
||||
```markdown
|
||||
![[_handwriting/hw_abc123.svg]]
|
||||
```
|
||||
Il file SVG è visibile come immagine anche senza il plugin. I tratti sono salvati come JSON in `<desc class="hwm-strokes">` dentro l'SVG.
|
||||
|
||||
**Legacy (backward compat):**
|
||||
````markdown
|
||||
```handwriting
|
||||
{"id":"hw_abc123","svg":"_handwriting/hw_abc123.svg"}
|
||||
```
|
||||
````
|
||||
|
||||
### Deploy
|
||||
|
||||
```bash
|
||||
# Sorgente plugin
|
||||
cd C:/Projects/pluginObsidian/handWrittenMarkdownConverter/HandTranscriptMd
|
||||
### Deploy (comandi copia-incolla per PowerShell)
|
||||
|
||||
```powershell
|
||||
# Build + deploy al vault locale (solo PC)
|
||||
node esbuild.config.mjs production && bash deploy.sh
|
||||
cd C:\Projects\pluginObsidian\handWrittenMarkdownConverter\HandTranscriptMd; node esbuild.config.mjs production; bash deploy.sh
|
||||
|
||||
# Build + deploy su Google Drive (per testare su tablet Android)
|
||||
node esbuild.config.mjs production && bash cloudDeploy.sh
|
||||
|
||||
# Vault locale di test
|
||||
C:/Projects/CLIENTI/IOTTI/IOTTI_APP/_docs/handwriting-to-markdown/
|
||||
# Plugin installato in .obsidian/plugins/handwriting-to-markdown/
|
||||
|
||||
# Vault Google Drive (sincronizzato con tablet)
|
||||
C:/Users/gabri/Il mio Drive (gabrielecusato@gmail.com)/Projects/handwriting-to-markdown/
|
||||
cd C:\Projects\pluginObsidian\handWrittenMarkdownConverter\HandTranscriptMd; node esbuild.config.mjs production; bash cloudDeploy.sh
|
||||
```
|
||||
|
||||
### Percorsi vault
|
||||
|
||||
- **Vault locale di test:** `C:\Projects\CLIENTI\IOTTI\IOTTI_APP\_docs\handwriting-to-markdown\`
|
||||
- Plugin in `.obsidian\plugins\handwriting-to-markdown\`
|
||||
- **Vault Google Drive (tablet):** `C:\Users\gabri\Il mio Drive (gabrielecusato@gmail.com)\Projects\handwriting-to-markdown\`
|
||||
|
||||
### Come sviluppare
|
||||
|
||||
```bash
|
||||
```powershell
|
||||
# Dev mode (watch)
|
||||
npm run dev
|
||||
cd C:\Projects\pluginObsidian\handWrittenMarkdownConverter\HandTranscriptMd; npm run dev
|
||||
|
||||
# Dopo ogni modifica per testare su PC:
|
||||
bash deploy.sh
|
||||
cd C:\Projects\pluginObsidian\handWrittenMarkdownConverter\HandTranscriptMd; node esbuild.config.mjs production; bash deploy.sh
|
||||
|
||||
# Dopo ogni modifica per testare su tablet Android:
|
||||
bash cloudDeploy.sh
|
||||
cd C:\Projects\pluginObsidian\handWrittenMarkdownConverter\HandTranscriptMd; node esbuild.config.mjs production; bash cloudDeploy.sh
|
||||
|
||||
# In Obsidian: Ctrl+P → "Reload app without saving"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Note architetturali — Fase 2 (sessione corrente)
|
||||
## Note architetturali
|
||||
|
||||
### Come funziona il flusso OCR
|
||||
|
||||
|
|
@ -151,7 +185,29 @@ bash cloudDeploy.sh
|
|||
|
||||
---
|
||||
|
||||
## Prossimi passi — Da fare nella prossima sessione
|
||||
## Prossimi passi
|
||||
|
||||
### BUG handwriting — Test da fare in ordine
|
||||
|
||||
**Test A — `pointer-events: none` sullo span** (1 riga, da provare per primo):
|
||||
- In `tryDecorate()`, aggiungere `span.style.pointerEvents = 'none'` dopo `span.dataset.hwmDecorated = '1'`
|
||||
- I bottoni portale (in `document.body`) continuano a funzionare
|
||||
- Verifica: con SVG riempito nel documento, l'handwriting funziona?
|
||||
- Se sì → risolto. Se no → Chrome usa DOM walk separato, pointer-events non basta
|
||||
|
||||
**Test B — Limitare max-height dell'immagine** (se A non funziona):
|
||||
- Ipotesi: SVG vuoto (300px) non rompe, SVG riempito (500px+) rompe → è la HEIGHT del CE=false che causa il problema
|
||||
- Test empirico: aggiungere in CSS `.internal-embed[src*="_handwriting/"] img { max-height: 300px; object-fit: contain; }` e verificare se con un SVG alto (dopo auto-expand) l'handwriting funziona
|
||||
- Se sì → limitare max-height è la soluzione
|
||||
- Se no → l'altezza non è la causa, passare a C
|
||||
|
||||
**Test C — Placeholder in Live Preview** (se B non funziona):
|
||||
- In live preview (span dentro `.cm-editor`), mostrare solo un piccolo badge "📝 disegno" di 40px di altezza invece dell'SVG pieno
|
||||
- L'SVG pieno appare solo in Reading View (span dentro `.markdown-reading-view`)
|
||||
- Come distinguerle: `span.closest('.cm-editor')` vs `span.closest('.markdown-reading-view')`
|
||||
- Questo riduce il CE=false a un'area minima → handwriting dovrebbe funzionare nelle righe di testo circostanti
|
||||
|
||||
### Altri task aperti
|
||||
|
||||
1. **Migliorare il riconoscimento dei caratteri speciali markdown** — `src/recognizer.ts` + `src/md-parser.ts`:
|
||||
- Il testo normale viene riconosciuto correttamente da Gemini
|
||||
|
|
@ -159,64 +215,95 @@ bash cloudDeploy.sh
|
|||
- Migliorare il prompt con few-shot examples espliciti per i simboli (es. "Se vedi `# Titolo` → scrivi `# Titolo`")
|
||||
- Valutare se `md-parser.ts` può coprire i casi che il prompt non gestisce
|
||||
|
||||
2. **Fix handwriting Android — IN CORSO (15+ tentativi)**:
|
||||
2. **Auto-expand — animazione scattosa** — **BUG APERTO** (parzialmente risolto):
|
||||
- Fix precedente: guard `if (this.animFrameId !== null) return` in `checkAutoExpand()` → loop up/down risolto
|
||||
- Problema residuo: espansione scattosa invece che fluida
|
||||
- File: `src/drawing-canvas.ts` → `animateHeight()` e `checkAutoExpand()`
|
||||
|
||||
### Problema fondamentale
|
||||
Il canvas vive dentro `cm-content[contenteditable="true"]` (l'editor CodeMirror di Obsidian). Android/Chrome attiva lo stylus handwriting-to-text a **livello nativo** quando il pennino tocca dentro/vicino (40dp) a un elemento `contenteditable`. Questo avviene **prima** di qualsiasi evento JavaScript.
|
||||
3. **Toolbar unificata Windows/Android** — **DA FARE**:
|
||||
- Attualmente la toolbar su Windows è sempre espansa, su Android è compatta con toggle ▼/▲
|
||||
- Obiettivo: unico codice toolbar condiviso, compatta di default su entrambe le piattaforme
|
||||
- File coinvolti: `src/editor-view.ts` + `styles.css`
|
||||
|
||||
### Perché è sempre "tutto o niente"
|
||||
`cm-content` è un UNICO elemento `contenteditable="true"`. Qualsiasi modifica all'editabilità di un figlio contamina lo stato dell'intero editor.
|
||||
### Problemi risolti
|
||||
|
||||
### Tentativi falliti e scoperte chiave
|
||||
|
||||
| # | Approccio | Risultato | Perché non funziona |
|
||||
|---|-----------|-----------|---------------------|
|
||||
| 1-8 | `touch-action: none` su `.cm-editor`/`.cm-scroller`/`.cm-content` | Handwriting resta attivo | `touch-action` controlla scroll/zoom, NON l'handwriting |
|
||||
| 9 | `contenteditable="false"` + `handwriting="false"` sul **container** (figlio di cm-content) | Handwriting morto ovunque | Chrome tratta l'intero cm-content come blocco; un figlio false rompe tutto |
|
||||
| 10 | CSS `-webkit-user-modify: read-only` sul container | Handwriting morto ovunque | Stesso problema: figlio dentro cm-content |
|
||||
| 11 | `blur()` + `focus(canvas)` su `pointerenter pen` | Handwriting resta attivo | Rilevamento basato su **prossimità** al contenteditable, non sul focus |
|
||||
| 12 | `touchstart` + `preventDefault()` + `touch-action: none` | Handwriting resta attivo | Chrome decide prima degli eventi JS |
|
||||
| 13 | **Lock Button** — `contenteditable="false"` sul **container** (sbagliato) | Handwriting non si blocca | Il container è dentro cm-content → sbagliato mirare al container |
|
||||
| 14 | **Iframe** — canvas dentro `<iframe>` con `document.write` | Iframe creato (confermato da debug), handwriting resta attivo | Android's handwriting detection lavora a livello WebView (un'unica View Android), vede attraverso gli iframe |
|
||||
| 15 | **Overlay fullscreen** — pannello `position:fixed` su `document.body` + `cm-content.contenteditable="false"` mentre overlay è aperto | **PARZIALMENTE TESTATO** — handwriting ancora attivo su vecchi documenti, non testato su documento nuovo | Da verificare: testare su documento nuovo appena creato; possibile che vecchi documenti abbiano stato residuo |
|
||||
|
||||
### Scoperta critica: `inputmode="none"` contamina il WebView
|
||||
Bug confermato ([Flutter #176913](https://github.com/flutter/flutter/issues/176913)): dare focus a un elemento con `inputmode="none"` disabilita l'handwriting per l'INTERO WebView, effetto persiste fino al riavvio. **MAI usare `inputmode="none"` nel plugin.**
|
||||
|
||||
### Anche il plugin Ink ha lo stesso problema
|
||||
[Issue #156](https://github.com/daledesilva/obsidian_ink/issues/156): "Writing just puts a dot and doesn't follow pen on Android" — stessa causa, nessuna soluzione trovata.
|
||||
|
||||
### Approccio attuale (tentativo 15) — DA TESTARE MEGLIO
|
||||
Overlay quasi-fullscreen che si apre al tap sul riquadro:
|
||||
- `document.body.appendChild(overlay)` → fuori da cm-content
|
||||
- Quando overlay apre → `cm-content.setAttribute('contenteditable', 'false')`
|
||||
- Quando overlay chiude → ripristina `contenteditable="true"`
|
||||
- Backdrop semitrasparente con `backdrop-filter: blur(6px)`
|
||||
- Pannello con margini 12px, bordi arrotondati, ombra
|
||||
- Bottone ← in alto a sinistra, tool a destra
|
||||
- File: `src/embed.ts` → `showMobilePreview()`, `openFullscreenEditor()`
|
||||
|
||||
### Miglioramenti overlay ancora da fare
|
||||
- **Bordi più spessi/visibili** sul pannello (attualmente `box-shadow` sottile)
|
||||
- **Scroll interno** quando l'altezza del canvas supera quella dello schermo
|
||||
- **Verificare handwriting** su documento NUOVO (non su documenti già aperti che potrebbero avere stato residuo)
|
||||
- **Verificare causa** se handwriting ancora attivo: potrebbe essere che `cm-content.contenteditable="false"` non è sufficiente da solo, oppure che il timing è sbagliato
|
||||
|
||||
3. **Auto-expand — animazione scattosa** — **BUG APERTO** (parzialmente risolto):
|
||||
- Fix precedente: aggiunto guard `if (this.animFrameId !== null) return` in `checkAutoExpand()` per evitare restart multipli → il loop up/down è risolto
|
||||
- Problema residuo: l'espansione avviene in modo scattoso invece di animarsi fluidamente
|
||||
- Effetto collaterale: quando il canvas si espande oltre il viewport, la pagina scrolla bruscamente verso il basso
|
||||
- File coinvolto: `src/drawing-canvas.ts` → `animateHeight()` e `checkAutoExpand()`
|
||||
|
||||
4. **Toolbar unificata Windows/Android** — **DA FARE**:
|
||||
- Attualmente la toolbar su Windows è sempre espansa e quella su Android è compatta con toggle ▼/▲
|
||||
- Obiettivo: unico codice toolbar condiviso — compatta di default su entrambe le piattaforme
|
||||
- File coinvolti: `src/embed.ts` + `styles.css`
|
||||
|
||||
### Problemi risolti in passato
|
||||
- **Handwriting Android (disegno)** ✅ — risolto con editor in tab separata (`ItemView`), canvas fuori da `cm-content`
|
||||
- **Pen scroll** ✅ — penna non scrolla più, solo dito (JS manuale via `setPointerCapture`)
|
||||
- **Toolbar — tema scuro** ✅
|
||||
- **Spazio vuoto sezione colori in toolbar compatta** ✅
|
||||
- **Trashcan non cancella visualmente** ✅
|
||||
- **Bottoni inline coprivano `</>` di Obsidian** ✅ — spostati a `left: 6px`
|
||||
- **Ordine bottoni inline** ✅ — invertito: X, Converti, Freccia (da sinistra)
|
||||
- **Placeholder text** ✅ — aggiornato a "Usa il bottone matita in alto a destra per disegnare"
|
||||
- **Bottone portale non cerchio perfetto** ✅ — risolto con `width/height/min-width/min-height: 36px !important`, `padding: 0 !important`, `overflow: hidden`
|
||||
- **Icona bottone portale non visibile** ✅ — SVG con `stroke="currentColor"` non diventava bianco; risolto con `.hwm_portal-btn svg { stroke: #ffffff !important }`
|
||||
- **Bottone portale non si nasconde con editor aperto** ✅ — aggiunto check `workspace.getLeavesOfType(VIEW_TYPE_HANDWRITING).some(...)` nel RAF loop
|
||||
- **Bottone portale `position: absolute` invece di `fixed`** ✅ — `getBoundingClientRect()` restituisce coordinate viewport, non serviva aggiungere `scrollY/scrollX`
|
||||
|
||||
### BUG APERTO — Handwriting disabilitato nel documento quando il riquadro è presente
|
||||
|
||||
**Sintomo**: quando nel documento è presente un riquadro handwriting con un disegno (SVG non vuoto), la stylus handwriting-to-text di Android smette di funzionare nell'intero editor. Cancellare il riquadro ripristina l'handwriting. Il problema persiste tra riavvii di Obsidian.
|
||||
|
||||
**Progressione delle scoperte**:
|
||||
|
||||
**Fase 1 — Formato code block** (tentativi 16-26):
|
||||
- `contenteditable="false"` su wrapper CM6 → rimosso → non risolve
|
||||
- `touch-action: none` → rimosso → non risolve
|
||||
- `background-image` SVG → rimossa → non risolve
|
||||
- `<canvas>` nel DOM → rimosso → non risolve
|
||||
- Canvas in `document.body` (fuori da CM6) toccato con stylus → **rompe handwriting** — conclusione: è il canvas element quando toccato dalla stylus, non la sua posizione nel DOM
|
||||
- **Causa root fase 1**: Android WebView tratta qualsiasi `<canvas>` toccato dalla stylus come "drawing surface" e disabilita handwriting-to-text a livello di sessione WebView
|
||||
|
||||
**Fase 2 — Passaggio a formato wiki `![[svg]]`** (sessione corrente):
|
||||
|
||||
L'obiettivo era eliminare il `<canvas>` dal documento e mostrare solo l'`<img>` nativa di Obsidian.
|
||||
|
||||
Problema riscontrato: l'SVG vuoto (300px di altezza) NON rompe l'handwriting. L'SVG con un disegno (altezza variabile dopo auto-expand) SÌ lo rompe — anche dopo riavvio Obsidian, anche senza mai aprire la tab editor.
|
||||
|
||||
Cambiamenti implementati durante la fase 2:
|
||||
- Passaggio da code block a `![[svg]]` come formato principale
|
||||
- `insertHandwritingBlock()` crea il file SVG PRIMA di inserire il wikilink (altrimenti Obsidian mostra "could not be found")
|
||||
- MutationObserver su `document.body` per intercettare gli span (il post-processor non funziona per i widget CM6 immagine in live preview)
|
||||
- Fix data-URI: `img.src = data:image/svg+xml,...` → cambiato in cache-bust URL (`?t=timestamp`) perché la data-URI veniva interpretata da Android come drawing surface
|
||||
- Rimosso `addWikiOverlay` (aggiungeva `hwm_inline-buttons` come figlio dello span): la struttura dello span è ora identica a un'immagine normale
|
||||
- Pannello portale (`hwm_portal-panel`) in `document.body` con tutti e 4 i bottoni
|
||||
|
||||
**Stato attuale**: dopo aver rimosso TUTTA la nostra decorazione dallo span (nessun figlio aggiunto, nessuna classe, nessuno stile), il problema persiste. Lo span è identico a quello di un'immagine normale Obsidian, ma l'handwriting si rompe ugualmente con l'SVG riempito.
|
||||
|
||||
**Ipotesi residue** (non ancora testate):
|
||||
|
||||
| # | Ipotesi | Razionale |
|
||||
|---|---------|-----------|
|
||||
| A | `pointer-events: none` sullo span | Se Chrome usa lo stesso hit-test dei pointer events per la proximity detection, potrebbe ignorare lo span e trovare il `cm-content[ce=true]` sottostante. Incerto: Chrome potrebbe fare un DOM walk separato per `contenteditable` indipendente da pointer-events |
|
||||
| B | Altezza SVG: l'empty SVG è 300px, il filled SVG è più alto (auto-expand). Un CE=false più alto copre più area → più probabile interferire con la proximity detection (40dp dal bordo). Fix: limitare max-height dell'immagine nel documento | La proximity detection è 40dp verticale — un'immagine da 500px di altezza occupa molto più "territorio" di una da 300px |
|
||||
| C | Placeholder in Live Preview | Mostrare solo un piccolo badge in live preview (dove si scrive), SVG pieno solo in Reading View. Lo span CE=false sarebbe piccolo → non interferisce |
|
||||
|
||||
**Fonti**:
|
||||
- [Chromium Stylus Handwriting README](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/stylus_handwriting/README.md)
|
||||
- [ProseMirror Issue #565](https://github.com/ProseMirror/prosemirror/issues/565)
|
||||
- Plugin Ink ha lo stesso problema irrisolto ([Issue #156](https://github.com/daledesilva/obsidian_ink/issues/156))
|
||||
|
||||
**Scoperte chiave consolidate**:
|
||||
- `inputmode="none"` contamina l'intero WebView — MAI usare
|
||||
- Empty SVG (300px) NON rompe handwriting; SVG con disegno (altezza > 300px per auto-expand) SÌ
|
||||
- data-URI come `img.src` rompe handwriting → usare sempre URL vault + cache-bust
|
||||
- Il problema è persistente tra sessioni (riavvio Obsidian) — non è corruzione di sessione temporanea
|
||||
- Rimuovere completamente la nostra decorazione (nessun figlio nello span) non risolve — la causa è nell'SVG stesso o nella sua altezza
|
||||
|
||||
**Tentativi falliti (completo)**:
|
||||
|
||||
| # | Approccio | Risultato |
|
||||
|---|-----------|-----------|
|
||||
| 16 | Rimuovere `beforeinput` listener globale | Non risolve |
|
||||
| 17 | `<button>` → `<div role="button">` in cm-content | Non risolve |
|
||||
| 18 | `<img>` → CSS `background-image` | Non risolve |
|
||||
| 19 | Rimuovere `contenteditable="false"` dai wrapper CM6 + `pointer-events: none` | Non risolve |
|
||||
| 20-23 | DevTools: rimuovere CE=false, touch-action, background-image, canvas dal DOM | Non risolve |
|
||||
| 24 | Editor in Modal invece di tab | Non praticabile (stylus non disegna nel modal) |
|
||||
| 25 | Bottone portale in `document.body` | Non risolve (canvas nella tab corrompe sessione) |
|
||||
| 26 | Test console: canvas fake in `document.body` toccato con stylus | Conferma: canvas + stylus = handwriting rotto |
|
||||
| 27 | Passaggio a `![[svg]]` con MutationObserver | Non risolve |
|
||||
| 28 | Rimozione totale decorazione dallo span (nessun figlio aggiunto) | Non risolve |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "handwriting-to-markdown",
|
||||
"name": "Handwriting to Markdown",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Inline handwriting canvas with conversion to structured markdown",
|
||||
"author": "GabrieleC",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
|
@ -0,0 +1,408 @@
|
|||
/* ========================================
|
||||
Handwriting to Markdown — Plugin Styles
|
||||
Prefisso hwm_ per evitare conflitti
|
||||
======================================== */
|
||||
|
||||
/* --- Container principale dell'embed --- */
|
||||
.hwm_container {
|
||||
position: relative;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin: 8px 0;
|
||||
/* Sfondo SEMPRE bianco (anche in dark mode) */
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* --- Toolbar in alto a destra --- */
|
||||
.hwm_toolbar {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0; /* spacing gestito via margin-right sui bottoni, così i bottoni
|
||||
nascosti (max-width:0, margin:0) non creano spazi vuoti */
|
||||
padding: 3px 6px;
|
||||
border-radius: 6px;
|
||||
background: rgba(240, 240, 240, 0.95);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
/* --- Bottoni toolbar --- */
|
||||
.hwm_btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
line-height: 0;
|
||||
/* Spacing via margin-right invece di gap, così i bottoni nascosti
|
||||
(margin:0) non lasciano spazi vuoti nella toolbar compatta */
|
||||
margin-right: 4px;
|
||||
/* Animazione espansione/compressione toolbar */
|
||||
overflow: hidden;
|
||||
max-width: 40px;
|
||||
flex-shrink: 0;
|
||||
transition: max-width 0.2s ease, opacity 0.2s ease,
|
||||
margin 0.2s ease, padding 0.2s ease;
|
||||
}
|
||||
/* Icone SVG inline dentro i bottoni */
|
||||
.hwm_btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.hwm_btn:hover {
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
/* Bottone attivo (tool selezionato) */
|
||||
.hwm_btn.hwm_active {
|
||||
background: var(--interactive-accent);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* --- Bottone salva (evidenziato) --- */
|
||||
.hwm_save-btn {
|
||||
margin-left: 4px;
|
||||
}
|
||||
.hwm_save-btn:hover {
|
||||
background: var(--interactive-accent);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* --- Bottone elimina (X rossa su hover) --- */
|
||||
.hwm_delete-btn:hover {
|
||||
background: #dc2626;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* --- Selettore colori --- */
|
||||
.hwm_colors {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
margin: 0 4px;
|
||||
}
|
||||
/* I pallini colore sono <div> (non <button>) per evitare che Obsidian Mobile
|
||||
sovrascriva width/height con i propri stili globali sui button.
|
||||
Le dimensioni (22px) sono forzate anche via style.setProperty inline in embed.ts */
|
||||
.hwm_color-btn {
|
||||
display: inline-block;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.hwm_color-btn.hwm_active {
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
.hwm_color-btn:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
/* --- Area canvas (editing) --- */
|
||||
.hwm_canvas-wrap {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.hwm_canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
cursor: crosshair;
|
||||
/* Sfondo bianco fisso */
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* --- Handle di resize in basso --- */
|
||||
.hwm_resize-handle {
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: ns-resize;
|
||||
background: #f5f5f5;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.hwm_resize-handle:hover {
|
||||
background: #ebebeb;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* --- Placeholder per canvas vuoto --- */
|
||||
.hwm_placeholder {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 40px 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* --- Errore parsing --- */
|
||||
.hwm_error {
|
||||
color: var(--text-error);
|
||||
padding: 8px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* --- Separatore visivo nella toolbar --- */
|
||||
.hwm_separator {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: #d0d0d0;
|
||||
margin: 0 6px 0 2px; /* destra più ampia per bilanciare visivamente */
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
transition: max-width 0.2s ease, opacity 0.2s ease,
|
||||
margin 0.2s ease, padding 0.2s ease;
|
||||
}
|
||||
|
||||
/* --- Pallini colore: transizione per animazione toolbar --- */
|
||||
.hwm_color-btn {
|
||||
transition: max-width 0.2s ease, opacity 0.2s ease,
|
||||
margin 0.2s ease, padding 0.2s ease;
|
||||
}
|
||||
|
||||
/* --- Toolbar compatta (mobile) ---
|
||||
Bottoni nascosti: collassati a larghezza 0 con fade-out.
|
||||
La transizione su .hwm_btn e .hwm_separator produce l'animazione
|
||||
quando la classe viene aggiunta/rimossa via JS. */
|
||||
.hwm_toolbar--compact .hwm_pen-btn,
|
||||
.hwm_toolbar--compact .hwm_eraser-btn,
|
||||
.hwm_toolbar--compact .hwm_clear-btn,
|
||||
.hwm_toolbar--compact .hwm_save-btn,
|
||||
.hwm_toolbar--compact .hwm_color-btn:not(.hwm_active),
|
||||
.hwm_toolbar--compact .hwm_separator {
|
||||
max-width: 0 !important;
|
||||
opacity: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* Riduce il margine attorno al pallino colore rimasto e azzera il gap
|
||||
tra i pallini (gli inattivi sono collassati a width:0 ma il gap persiste) */
|
||||
.hwm_toolbar--compact .hwm_colors {
|
||||
margin: 0 2px;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* --- Tema scuro per la toolbar ---
|
||||
Applicato via classe hwm_toolbar--dark quando bgMode === 'dark' */
|
||||
.hwm_toolbar--dark {
|
||||
background: rgba(40, 40, 40, 0.97) !important;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4) !important;
|
||||
}
|
||||
.hwm_toolbar--dark .hwm_btn {
|
||||
color: #bbb !important;
|
||||
}
|
||||
.hwm_toolbar--dark .hwm_btn:hover {
|
||||
background: rgba(255, 255, 255, 0.12) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.hwm_toolbar--dark .hwm_btn.hwm_active {
|
||||
background: var(--interactive-accent) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.hwm_toolbar--dark .hwm_separator {
|
||||
background: #555 !important;
|
||||
}
|
||||
|
||||
/* --- Mobile: bottoni grandi e visibili ---
|
||||
Forza sfondo chiaro + colore scuro per le icone SVG,
|
||||
indipendentemente dal tema di Obsidian Mobile */
|
||||
.hwm_mobile .hwm_toolbar {
|
||||
gap: 0;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
.hwm_mobile .hwm_toolbar .hwm_btn {
|
||||
width: 36px !important;
|
||||
height: 36px !important;
|
||||
margin-right: 6px !important; /* spacing via margin, non gap */
|
||||
background: rgba(245, 245, 245, 0.98) !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.18) !important;
|
||||
border-radius: 7px !important;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18) !important;
|
||||
color: #222 !important;
|
||||
}
|
||||
.hwm_mobile .hwm_toolbar .hwm_btn.hwm_active {
|
||||
background: var(--interactive-accent) !important;
|
||||
border-color: var(--interactive-accent) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* --- Tema scuro su mobile: sovrascrive i bianchi !important del mobile --- */
|
||||
.hwm_mobile .hwm_toolbar--dark .hwm_btn {
|
||||
background: rgba(55, 55, 55, 0.98) !important;
|
||||
border-color: rgba(255, 255, 255, 0.15) !important;
|
||||
color: #ddd !important;
|
||||
}
|
||||
.hwm_mobile .hwm_toolbar--dark .hwm_btn.hwm_active {
|
||||
background: var(--interactive-accent) !important;
|
||||
border-color: var(--interactive-accent) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
INLINE PREVIEW — Vista nel documento
|
||||
======================================== */
|
||||
|
||||
/* --- Bottoni inline (comprimi, converti, elimina) --- */
|
||||
.hwm_inline-buttons {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 3px;
|
||||
border-radius: 6px;
|
||||
background: rgba(240, 240, 240, 0.9);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.hwm_inline-buttons--dark {
|
||||
background: rgba(40, 40, 40, 0.95);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.hwm_inline-buttons--dark .hwm_btn {
|
||||
color: #bbb;
|
||||
}
|
||||
.hwm_inline-buttons--dark .hwm_btn:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* --- Bottone comprimi: rotazione 180° quando compresso --- */
|
||||
.hwm_collapse-btn {
|
||||
transition: transform 0.3s ease, max-width 0.2s ease,
|
||||
opacity 0.2s ease, margin 0.2s ease, padding 0.2s ease;
|
||||
}
|
||||
.hwm_collapse-btn.hwm_rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* --- Preview SVG inline --- */
|
||||
.hwm_inline-preview {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
/* Transizione per l'espansione/compressione */
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
.hwm_inline-preview.hwm_collapsed {
|
||||
/* max-height impostato via JS */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Immagine SVG che riempie la larghezza del container */
|
||||
.hwm_preview-img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
pointer-events: none; /* il click è gestito dal div padre */
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
OVERLAY — Editor fullscreen
|
||||
======================================== */
|
||||
|
||||
/* Blocca scroll sul body quando l'overlay è aperto */
|
||||
body.hwm_no-scroll {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/* --- Backdrop: sfondo scuro + blur --- */
|
||||
.hwm_overlay-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
/* --- Pannello editor con cornice spessa --- */
|
||||
.hwm_overlay-panel {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
border: 4px solid #444;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.hwm_overlay-panel--dark {
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
/* --- Top bar (bottone ← + toolbar) --- */
|
||||
.hwm_overlay-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 8px;
|
||||
background: rgba(240, 240, 240, 0.95);
|
||||
border-bottom: 1px solid #ddd;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
}
|
||||
.hwm_overlay-topbar--dark {
|
||||
background: rgba(40, 40, 40, 0.97);
|
||||
border-bottom-color: #444;
|
||||
}
|
||||
|
||||
/* --- Bottone ← (indietro) --- */
|
||||
.hwm_back-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* --- Toolbar dentro l'overlay: posizione statica (non absolute) --- */
|
||||
.hwm_overlay-toolbar-inner {
|
||||
position: static !important;
|
||||
top: auto !important;
|
||||
right: auto !important;
|
||||
flex: 1;
|
||||
box-shadow: none !important;
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* --- Scroll container per il canvas nell'overlay --- */
|
||||
.hwm_overlay-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* --- Resize handle disabilitato (visibile ma non interattivo) --- */
|
||||
.hwm_resize-handle--disabled {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
.hwm_resize-handle--disabled:hover {
|
||||
background: #f5f5f5; /* annulla l'effetto hover */
|
||||
}
|
||||
|
|
@ -2,11 +2,15 @@
|
|||
# Deploy plugin files to Google Drive vault (sync cloud)
|
||||
# Uso: bash cloudDeploy.sh
|
||||
|
||||
# MSYS_NO_PATHCONV=1 impedisce a Git Bash di convertire i path C:/ in /c/
|
||||
# ed evita che mkdir crei una cartella "C:" relativa
|
||||
export MSYS_NO_PATHCONV=1
|
||||
|
||||
VAULT_PLUGIN="C:/Users/gabri/Il mio Drive (gabrielecusato@gmail.com)/Projects/handwriting-to-markdown"
|
||||
SRC_DIR="$(dirname "$0")"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
mkdir -p "$VAULT_PLUGIN"
|
||||
cp "$SRC_DIR/main.js" "$SRC_DIR/manifest.json" "$SRC_DIR/styles.css" "$VAULT_PLUGIN/"
|
||||
cp "$SCRIPT_DIR/main.js" "$SCRIPT_DIR/manifest.json" "$SCRIPT_DIR/styles.css" "$VAULT_PLUGIN/"
|
||||
|
||||
echo "Deployed to $VAULT_PLUGIN"
|
||||
echo " main.js $(wc -c < "$VAULT_PLUGIN/main.js") bytes"
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@
|
|||
# Deploy plugin files to Obsidian vault
|
||||
# Uso: bash deploy.sh
|
||||
|
||||
export MSYS_NO_PATHCONV=1
|
||||
|
||||
VAULT_PLUGIN="C:/Projects/CLIENTI/IOTTI/IOTTI_APP/_docs/handwriting-to-markdown/.obsidian/plugins/handwriting-to-markdown"
|
||||
SRC_DIR="$(dirname "$0")"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
mkdir -p "$VAULT_PLUGIN"
|
||||
cp "$SRC_DIR/main.js" "$SRC_DIR/manifest.json" "$SRC_DIR/styles.css" "$VAULT_PLUGIN/"
|
||||
cp "$SCRIPT_DIR/main.js" "$SCRIPT_DIR/manifest.json" "$SCRIPT_DIR/styles.css" "$VAULT_PLUGIN/"
|
||||
|
||||
echo "Deployed to $VAULT_PLUGIN"
|
||||
echo " main.js $(wc -c < "$VAULT_PLUGIN/main.js") bytes"
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ export class DrawingCanvas {
|
|||
private historyIdx = -1;
|
||||
// Flag per sapere se la gomma ha modificato qualcosa durante un drag
|
||||
private eraserChanged = false;
|
||||
// Callback invocato quando l'altezza del canvas cambia (auto-expand)
|
||||
private resizeCb: (() => void) | null = null;
|
||||
|
||||
// Altezza di default delle settings (usata per reset su clear)
|
||||
private defaultHeight: number;
|
||||
|
|
@ -66,9 +68,6 @@ export class DrawingCanvas {
|
|||
private boundDown: (e: PointerEvent) => void;
|
||||
private boundMove: (e: PointerEvent) => void;
|
||||
private boundUp: (e: PointerEvent) => void;
|
||||
private boundBeforeInput: ((e: Event) => void) | null = null;
|
||||
// Riferimento al container per il check "tap fuori"
|
||||
private container: HTMLElement | null = null;
|
||||
// Callback debug: se impostato, mostra Notice all'utente per ogni evento IME/touch
|
||||
private debugFn: ((msg: string) => void) | null = null;
|
||||
|
||||
|
|
@ -78,19 +77,7 @@ export class DrawingCanvas {
|
|||
this.canvas.height = height;
|
||||
this.defaultHeight = defaultHeight;
|
||||
this.mobileMode = mobileMode;
|
||||
this.container = container;
|
||||
this.debugFn = debugFn;
|
||||
// HANDWRITING ANDROID — problema aperto, vedi CLAUDE.md per storico tentativi.
|
||||
// Safety net: blocca beforeinput durante il disegno (utile anche in fullscreen)
|
||||
if (mobileMode) {
|
||||
this.boundBeforeInput = (e: Event) => {
|
||||
if (this.isDrawing) {
|
||||
this.debugFn?.('🚫 beforeinput bloccato');
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
document.addEventListener('beforeinput', this.boundBeforeInput, true);
|
||||
}
|
||||
this.canvas.classList.add('hwm_canvas');
|
||||
// touch-action: none sul canvas previene scroll/zoom durante il disegno
|
||||
this.canvas.style.setProperty('touch-action', 'none', 'important');
|
||||
|
|
@ -115,6 +102,37 @@ export class DrawingCanvas {
|
|||
/* --- API pubblica --- */
|
||||
|
||||
onChange(cb: () => void) { this.changeCb = cb; }
|
||||
// Registra callback per quando l'altezza cambia (utile per auto-scroll nell'overlay)
|
||||
onResize(cb: () => void) { this.resizeCb = cb; }
|
||||
// Abilita scroll manuale con il dito sul canvas.
|
||||
// touch-action resta 'none' (la penna non trigga scroll del browser),
|
||||
// il dito scrolla il container via JS.
|
||||
allowFingerScroll(scrollContainer: HTMLElement) {
|
||||
let scrolling = false;
|
||||
let startY = 0;
|
||||
let startScroll = 0;
|
||||
|
||||
this.canvas.addEventListener('pointerdown', (e: PointerEvent) => {
|
||||
if ((e.pointerType || 'pen') !== 'touch') return;
|
||||
scrolling = true;
|
||||
startY = e.clientY;
|
||||
startScroll = scrollContainer.scrollTop;
|
||||
this.canvas.setPointerCapture(e.pointerId);
|
||||
});
|
||||
|
||||
this.canvas.addEventListener('pointermove', (e: PointerEvent) => {
|
||||
if (!scrolling || (e.pointerType || 'pen') !== 'touch') return;
|
||||
e.preventDefault();
|
||||
scrollContainer.scrollTop = startScroll + (startY - e.clientY);
|
||||
});
|
||||
|
||||
const stop = (e: PointerEvent) => {
|
||||
if ((e.pointerType || 'pen') !== 'touch') return;
|
||||
scrolling = false;
|
||||
};
|
||||
this.canvas.addEventListener('pointerup', stop);
|
||||
this.canvas.addEventListener('pointerleave', stop);
|
||||
}
|
||||
|
||||
setMode(mode: DrawMode) { this.mode = mode; }
|
||||
getMode(): DrawMode { return this.mode; }
|
||||
|
|
@ -187,11 +205,6 @@ export class DrawingCanvas {
|
|||
this.canvas.removeEventListener('pointermove', this.boundMove);
|
||||
this.canvas.removeEventListener('pointerup', this.boundUp);
|
||||
this.canvas.removeEventListener('pointerleave', this.boundUp);
|
||||
// Rimuove listener beforeinput (registrato solo in mobileMode)
|
||||
if (this.boundBeforeInput) {
|
||||
document.removeEventListener('beforeinput', this.boundBeforeInput, true);
|
||||
this.boundBeforeInput = null;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- History --- */
|
||||
|
|
@ -312,6 +325,8 @@ export class DrawingCanvas {
|
|||
if (this.currentStroke) {
|
||||
this.drawFullStroke(this.currentStroke);
|
||||
}
|
||||
// Notifica chi ascolta (overlay auto-scroll)
|
||||
this.resizeCb?.();
|
||||
|
||||
if (progress < 1) {
|
||||
this.animFrameId = requestAnimationFrame(step);
|
||||
|
|
|
|||
675
HandTranscriptMd/src/editor-view.ts
Normal file
675
HandTranscriptMd/src/editor-view.ts
Normal file
|
|
@ -0,0 +1,675 @@
|
|||
/* =============================================
|
||||
DrawingEditorView — Editor in tab Obsidian
|
||||
Apre il canvas in una tab dedicata, fuori dal
|
||||
DOM di CodeMirror → nessun conflitto
|
||||
handwriting Android.
|
||||
============================================= */
|
||||
|
||||
import { ItemView, WorkspaceLeaf, TFile, Notice, Platform, Modal, App } from 'obsidian';
|
||||
import type HandwritingPlugin from './main';
|
||||
import { DrawingCanvas, Stroke } from './drawing-canvas';
|
||||
import { strokesToSvg, parseSvgStrokes } from './svg-utils';
|
||||
import { getEffectiveBgColor, getEffectiveLineColor, remapStrokeColor } from './settings';
|
||||
import { getRecognizer } from './recognizer';
|
||||
import { parseMarkdown } from './md-parser';
|
||||
|
||||
export const VIEW_TYPE_HANDWRITING = 'handwriting-editor';
|
||||
|
||||
// Icone SVG inline (stile Lucide 24×24)
|
||||
const ICONS: Record<string, string> = {
|
||||
'pencil': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>`,
|
||||
'eraser': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/><path d="M22 21H7"/><path d="m5 11 9 9"/></svg>`,
|
||||
'rotate-ccw': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>`,
|
||||
'rotate-cw': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>`,
|
||||
'trash': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>`,
|
||||
'file-text': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>`,
|
||||
'save': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/><path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"/><path d="M7 3v4a1 1 0 0 0 1 1h7"/></svg>`,
|
||||
'x': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>`,
|
||||
'chevron-down':`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>`,
|
||||
'chevron-up': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>`,
|
||||
'arrow-left': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>`,
|
||||
};
|
||||
|
||||
export class DrawingEditorView extends ItemView {
|
||||
plugin: HandwritingPlugin;
|
||||
private canvas: DrawingCanvas | null = null;
|
||||
private embedId = '';
|
||||
private svgPath = '';
|
||||
private sourcePath = '';
|
||||
private saveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: HandwritingPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
getViewType() { return VIEW_TYPE_HANDWRITING; }
|
||||
getDisplayText() { return 'Handwriting Editor'; }
|
||||
getIcon() { return 'pencil'; }
|
||||
getEmbedId() { return this.embedId; }
|
||||
|
||||
async setState(state: any, result: any) {
|
||||
if (state?.id) this.embedId = state.id;
|
||||
if (state?.svg) this.svgPath = state.svg;
|
||||
if (state?.sourcePath) this.sourcePath = state.sourcePath;
|
||||
// Costruisci la UI solo quando abbiamo i dati
|
||||
if (this.embedId && this.svgPath) await this.buildEditor();
|
||||
await super.setState(state, result);
|
||||
}
|
||||
|
||||
getState() {
|
||||
return { id: this.embedId, svg: this.svgPath, sourcePath: this.sourcePath };
|
||||
}
|
||||
|
||||
async onOpen() { /* UI costruita in setState */ }
|
||||
|
||||
async onClose() {
|
||||
if (this.canvas) {
|
||||
await this.saveSvg();
|
||||
this.canvas.destroy();
|
||||
this.canvas = null;
|
||||
}
|
||||
if (this.saveTimer) clearTimeout(this.saveTimer);
|
||||
}
|
||||
|
||||
/* ---------- Costruisce la UI dell'editor ---------- */
|
||||
|
||||
private async buildEditor() {
|
||||
const el = this.contentEl;
|
||||
el.empty();
|
||||
el.classList.add('hwm_editor-view');
|
||||
|
||||
const isMobile = Platform.isMobile;
|
||||
const isDark = this.plugin.settings.bgMode === 'dark';
|
||||
const bgColor = getEffectiveBgColor(this.plugin.settings);
|
||||
const lineColor = getEffectiveLineColor(this.plugin.settings);
|
||||
el.style.backgroundColor = bgColor;
|
||||
|
||||
// --- Top bar: ← a sinistra | toolbar a destra ---
|
||||
const topbar = el.createDiv({ cls: 'hwm_editor-topbar' });
|
||||
if (isDark) topbar.classList.add('hwm_editor-topbar--dark');
|
||||
|
||||
// Bottone ← (torna al documento)
|
||||
const backBtn = this.mkBtn(topbar, 'arrow-left', 'Torna al documento');
|
||||
backBtn.classList.add('hwm_back-btn');
|
||||
|
||||
// Toolbar (allineata a destra dentro il topbar)
|
||||
const toolbar = topbar.createDiv({ cls: 'hwm_toolbar hwm_editor-toolbar' });
|
||||
if (isDark) toolbar.classList.add('hwm_toolbar--dark');
|
||||
|
||||
// Toggle compatta (mobile)
|
||||
if (isMobile) {
|
||||
toolbar.classList.add('hwm_toolbar--compact');
|
||||
const toggleBtn = this.mkBtn(toolbar, 'chevron-down', 'Mostra tutti');
|
||||
toggleBtn.classList.add('hwm_toggle-btn');
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
const compact = toolbar.classList.toggle('hwm_toolbar--compact');
|
||||
updateColorSizes(compact);
|
||||
toggleBtn.innerHTML = ICONS[compact ? 'chevron-down' : 'chevron-up'] ?? '';
|
||||
});
|
||||
}
|
||||
|
||||
// Penna / Gomma
|
||||
const penBtn = this.mkBtn(toolbar, 'pencil', 'Penna');
|
||||
penBtn.classList.add('hwm_active', 'hwm_pen-btn');
|
||||
const eraserBtn = this.mkBtn(toolbar, 'eraser', 'Gomma');
|
||||
eraserBtn.classList.add('hwm_eraser-btn');
|
||||
toolbar.createDiv({ cls: 'hwm_separator' });
|
||||
|
||||
// Colori
|
||||
const colors = isDark
|
||||
? ['#ffffff', '#60a5fa', '#f87171', '#4ade80']
|
||||
: ['#000000', '#1e40af', '#dc2626', '#16a34a'];
|
||||
const colorWrap = toolbar.createDiv({ cls: 'hwm_colors' });
|
||||
const colorBtns: HTMLElement[] = [];
|
||||
for (const c of colors) {
|
||||
const btn = colorWrap.createEl('div', {
|
||||
cls: 'hwm_color-btn',
|
||||
attr: { title: c, role: 'button', tabindex: '0' }
|
||||
});
|
||||
btn.style.backgroundColor = c;
|
||||
// Dimensioni forzate (bypass stili Obsidian Mobile)
|
||||
for (const [k, v] of Object.entries({
|
||||
width: '22px', height: '22px', 'min-width': '22px',
|
||||
'min-height': '22px', 'border-radius': '50%',
|
||||
'box-sizing': 'border-box', 'flex-shrink': '0'
|
||||
})) btn.style.setProperty(k, v, 'important');
|
||||
if (c === colors[0]) btn.classList.add('hwm_active');
|
||||
colorBtns.push(btn);
|
||||
}
|
||||
toolbar.createDiv({ cls: 'hwm_separator' });
|
||||
|
||||
// Helper per dimensioni pallini in toolbar compatta
|
||||
const updateColorSizes = (compact: boolean) => {
|
||||
colorBtns.forEach(b => {
|
||||
const sz = (!compact || b.classList.contains('hwm_active')) ? '22px' : '0';
|
||||
b.style.setProperty('min-width', sz, 'important');
|
||||
b.style.setProperty('min-height', sz, 'important');
|
||||
});
|
||||
};
|
||||
if (isMobile) updateColorSizes(true);
|
||||
|
||||
// Undo / Redo / Clear
|
||||
const undoBtn = this.mkBtn(toolbar, 'rotate-ccw', 'Annulla');
|
||||
undoBtn.classList.add('hwm_undo-btn');
|
||||
const redoBtn = this.mkBtn(toolbar, 'rotate-cw', 'Ripristina');
|
||||
redoBtn.classList.add('hwm_redo-btn');
|
||||
const clearBtn = this.mkBtn(toolbar, 'trash', 'Cancella tutto');
|
||||
clearBtn.classList.add('hwm_clear-btn');
|
||||
toolbar.createDiv({ cls: 'hwm_separator' });
|
||||
|
||||
// Converti / Salva / Elimina
|
||||
const convertBtn = this.mkBtn(toolbar, 'file-text', 'Converti in Markdown');
|
||||
convertBtn.classList.add('hwm_convert-btn');
|
||||
const saveBtn = this.mkBtn(toolbar, 'save', 'Salva');
|
||||
saveBtn.classList.add('hwm_save-btn');
|
||||
const deleteBtn = this.mkBtn(toolbar, 'x', 'Elimina riquadro');
|
||||
deleteBtn.classList.add('hwm_delete-btn');
|
||||
|
||||
// --- Scroll container ---
|
||||
const scrollWrap = el.createDiv({ cls: 'hwm_editor-scroll' });
|
||||
const canvasWrap = scrollWrap.createDiv({ cls: 'hwm_canvas-wrap' });
|
||||
|
||||
// Carica tratti dal file SVG
|
||||
const { strokes, canvasHeight: savedH } = await this.loadStrokes();
|
||||
const { canvasWidth, canvasHeight } = this.plugin.settings;
|
||||
const h = savedH ?? canvasHeight;
|
||||
const debugFn = this.plugin.settings.debugMode
|
||||
? (msg: string) => new Notice(msg, 3000) : null;
|
||||
|
||||
// Crea il canvas
|
||||
this.canvas = new DrawingCanvas(canvasWrap, canvasWidth, h, canvasHeight, isMobile, debugFn);
|
||||
this.canvas.setBackground(bgColor, lineColor);
|
||||
this.canvas.setColor(colors[0]!);
|
||||
// Su mobile: dito = scroll, penna = disegno
|
||||
// Su mobile: dito = scroll manuale del container, penna = disegno
|
||||
if (isMobile) this.canvas.allowFingerScroll(scrollWrap);
|
||||
|
||||
// Carica tratti con remapping colori
|
||||
if (strokes.length > 0) {
|
||||
const remapped = strokes.map(s => ({
|
||||
...s, color: remapStrokeColor(s.color, this.plugin.settings.bgMode)
|
||||
}));
|
||||
this.canvas.loadStrokes(remapped);
|
||||
}
|
||||
|
||||
// Resize handle (visibile ma non interattivo)
|
||||
const handle = scrollWrap.createDiv({ cls: 'hwm_resize-handle hwm_resize-handle--disabled' });
|
||||
handle.createEl('span', { text: '⋯' });
|
||||
if (isDark) {
|
||||
handle.style.background = '#2a2a2a';
|
||||
handle.style.borderTopColor = '#444';
|
||||
handle.style.color = '#888';
|
||||
}
|
||||
|
||||
// Auto-scroll quando il canvas si espande
|
||||
this.canvas.onResize(() => { scrollWrap.scrollTop = scrollWrap.scrollHeight; });
|
||||
|
||||
// --- Event handlers ---
|
||||
const cv = this.canvas;
|
||||
|
||||
penBtn.addEventListener('click', () => {
|
||||
cv.setMode('pen');
|
||||
penBtn.classList.add('hwm_active');
|
||||
eraserBtn.classList.remove('hwm_active');
|
||||
});
|
||||
eraserBtn.addEventListener('click', () => {
|
||||
cv.setMode('eraser');
|
||||
eraserBtn.classList.add('hwm_active');
|
||||
penBtn.classList.remove('hwm_active');
|
||||
});
|
||||
for (let i = 0; i < colorBtns.length; i++) {
|
||||
colorBtns[i]!.addEventListener('click', () => {
|
||||
colorBtns.forEach(b => b.classList.remove('hwm_active'));
|
||||
colorBtns[i]!.classList.add('hwm_active');
|
||||
cv.setColor(colors[i]!);
|
||||
if (isMobile) updateColorSizes(toolbar.classList.contains('hwm_toolbar--compact'));
|
||||
});
|
||||
}
|
||||
undoBtn.addEventListener('click', () => cv.undo());
|
||||
redoBtn.addEventListener('click', () => cv.redo());
|
||||
clearBtn.addEventListener('click', () => cv.clear());
|
||||
|
||||
convertBtn.addEventListener('click', () => this.doConvert());
|
||||
saveBtn.addEventListener('click', async () => { await this.saveSvg(); new Notice('Salvato'); });
|
||||
deleteBtn.addEventListener('click', () => this.doDelete());
|
||||
|
||||
// Auto-save debounced (2s dopo ultimo cambiamento)
|
||||
cv.onChange(() => {
|
||||
if (this.saveTimer) clearTimeout(this.saveTimer);
|
||||
this.saveTimer = setTimeout(() => this.saveSvg(), 2000);
|
||||
});
|
||||
|
||||
// Bottone ← → salva e chiudi la tab
|
||||
backBtn.addEventListener('click', async () => {
|
||||
await this.saveSvg();
|
||||
this.leaf.detach();
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- File I/O ---------- */
|
||||
|
||||
private async loadStrokes(): Promise<{ strokes: Stroke[]; canvasHeight: number | null }> {
|
||||
const file = this.app.vault.getAbstractFileByPath(this.svgPath);
|
||||
if (file instanceof TFile) {
|
||||
const content = await this.app.vault.read(file);
|
||||
const strokes = parseSvgStrokes(content);
|
||||
const m = content.match(/viewBox="0 0 \d+ (\d+)"/);
|
||||
return { strokes, canvasHeight: m ? parseInt(m[1] ?? '0') : null };
|
||||
}
|
||||
return { strokes: [], canvasHeight: null };
|
||||
}
|
||||
|
||||
private async saveSvg() {
|
||||
if (!this.canvas) return;
|
||||
const strokes = this.canvas.getStrokes();
|
||||
const svg = strokesToSvg(strokes, this.canvas.getWidth(), this.canvas.getHeight(),
|
||||
this.canvas.getBgColor(), this.canvas.getLineColor());
|
||||
|
||||
// Crea cartella se necessario
|
||||
const folder = this.svgPath.substring(0, this.svgPath.lastIndexOf('/'));
|
||||
if (folder && !this.app.vault.getAbstractFileByPath(folder)) {
|
||||
await this.app.vault.createFolder(folder);
|
||||
}
|
||||
const existing = this.app.vault.getAbstractFileByPath(this.svgPath);
|
||||
if (existing instanceof TFile) {
|
||||
await this.app.vault.modify(existing, svg);
|
||||
} else {
|
||||
await this.app.vault.create(this.svgPath, svg);
|
||||
}
|
||||
// Aggiorna preview inline se visibile
|
||||
this.plugin.refreshPreview(this.embedId, svg);
|
||||
}
|
||||
|
||||
/* ---------- Converti OCR ---------- */
|
||||
|
||||
private async doConvert() {
|
||||
if (!this.canvas || this.canvas.getStrokes().length === 0) {
|
||||
new Notice('Nessun tratto da convertire');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
new Notice('Riconoscimento in corso…');
|
||||
const svg = strokesToSvg(this.canvas.getStrokes(), this.canvas.getWidth(),
|
||||
this.canvas.getHeight(), this.canvas.getBgColor(), this.canvas.getLineColor());
|
||||
// SVG → PNG base64
|
||||
const parser = new DOMParser();
|
||||
const svgEl = parser.parseFromString(svg, 'image/svg+xml').documentElement as unknown as SVGElement;
|
||||
const base64 = await this.svgToPng(svgEl);
|
||||
// OCR via Gemini
|
||||
const recognizer = getRecognizer(this.plugin.settings.geminiApiKey, this.plugin.settings.ocrLanguages);
|
||||
const rawText = await recognizer.recognize(base64);
|
||||
if (!rawText.trim()) { new Notice('Nessun testo riconosciuto'); return; }
|
||||
// Markdown + archivia + sostituisci
|
||||
const markdown = parseMarkdown(rawText);
|
||||
await this.archiveSvg();
|
||||
await this.replaceCodeBlock(markdown);
|
||||
this.canvas.destroy();
|
||||
this.canvas = null;
|
||||
this.leaf.detach();
|
||||
new Notice('Conversione completata!');
|
||||
} catch (e: unknown) {
|
||||
new Notice('Errore OCR: ' + (e instanceof Error ? e.message : String(e)));
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Elimina ---------- */
|
||||
|
||||
private async doDelete() {
|
||||
if (!confirm('Eliminare questo riquadro handwriting e il file SVG associato?')) return;
|
||||
if (this.canvas) { this.canvas.destroy(); this.canvas = null; }
|
||||
// Rimuovi code block dal .md
|
||||
await this.removeCodeBlock();
|
||||
// Cancella il file SVG
|
||||
const svgFile = this.app.vault.getAbstractFileByPath(this.svgPath);
|
||||
if (svgFile instanceof TFile) await this.app.vault.delete(svgFile);
|
||||
this.leaf.detach();
|
||||
new Notice('Riquadro eliminato');
|
||||
}
|
||||
|
||||
/* ---------- Manipolazione file .md ---------- */
|
||||
|
||||
private async archiveSvg() {
|
||||
const svgFile = this.app.vault.getAbstractFileByPath(this.svgPath);
|
||||
if (!(svgFile instanceof TFile)) return;
|
||||
const now = new Date();
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
const ts = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}` +
|
||||
`_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
|
||||
const dest = `${this.plugin.settings.svgFolder}/_converted`;
|
||||
if (!this.app.vault.getAbstractFileByPath(dest)) await this.app.vault.createFolder(dest);
|
||||
await this.app.vault.rename(svgFile, `${dest}/${ts}.svg`);
|
||||
}
|
||||
|
||||
// Regex per trovare ![[svgPath]] nel file .md (nuovo formato wiki)
|
||||
private wikiEmbedRegex(): RegExp {
|
||||
const escaped = this.svgPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
return new RegExp(`\\n?!\\[\\[${escaped}\\]\\]\\n?`);
|
||||
}
|
||||
|
||||
// Regex per trovare il code block legacy con l'id specifico
|
||||
private codeBlockRegex(): RegExp {
|
||||
const escaped = this.embedId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
return new RegExp(
|
||||
'\\n?```handwriting\\n.*?"id"\\s*:\\s*"' + escaped + '".*?\\n```\\n?', 's'
|
||||
);
|
||||
}
|
||||
|
||||
// Applica una sostituzione sul file .md, tentando prima il formato
|
||||
// wiki ![[svg]] e poi il code block legacy come fallback.
|
||||
// markdown === null → rimozione; stringa → sostituzione con il testo.
|
||||
private async replaceInMd(markdown: string | null) {
|
||||
const mdFile = this.app.vault.getAbstractFileByPath(this.sourcePath);
|
||||
if (!(mdFile instanceof TFile)) { new Notice('File markdown non trovato'); return; }
|
||||
|
||||
const content = await this.app.vault.read(mdFile);
|
||||
const wikiRegex = this.wikiEmbedRegex();
|
||||
const cbRegex = this.codeBlockRegex();
|
||||
const repl = markdown === null ? '\n' : '\n' + markdown + '\n';
|
||||
|
||||
// Prova prima il formato wiki; se non trova, prova il code block legacy
|
||||
let updated = content.replace(wikiRegex, repl);
|
||||
if (updated === content) updated = content.replace(cbRegex, repl);
|
||||
|
||||
if (updated !== content) await this.app.vault.modify(mdFile, updated);
|
||||
}
|
||||
|
||||
private async replaceCodeBlock(markdown: string) {
|
||||
await this.replaceInMd(markdown);
|
||||
}
|
||||
|
||||
private async removeCodeBlock() {
|
||||
await this.replaceInMd(null);
|
||||
}
|
||||
|
||||
/* ---------- Helpers ---------- */
|
||||
|
||||
private mkBtn(parent: HTMLElement, icon: string, title: string): HTMLElement {
|
||||
const btn = parent.createEl('button', { cls: 'hwm_btn', attr: { title } });
|
||||
btn.innerHTML = ICONS[icon] ?? '';
|
||||
return btn;
|
||||
}
|
||||
|
||||
// Converte SVGElement → PNG base64 via canvas HTML temporaneo
|
||||
private svgToPng(svgElement: SVGElement): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cvs = document.createElement('canvas');
|
||||
const ctx = cvs.getContext('2d')!;
|
||||
const img = new Image();
|
||||
const blob = new Blob(
|
||||
[new XMLSerializer().serializeToString(svgElement)],
|
||||
{ type: 'image/svg+xml' }
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
img.onload = () => {
|
||||
cvs.width = img.width; cvs.height = img.height;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
URL.revokeObjectURL(url);
|
||||
resolve(cvs.toDataURL('image/png').split(',')[1]!);
|
||||
};
|
||||
img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('SVG → PNG fallito')); };
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
DrawingModal — Editor disegno come Modal overlay.
|
||||
Aperto tramite bottone portale (document.body)
|
||||
per evitare tap su widget CM6.
|
||||
============================================= */
|
||||
|
||||
export class DrawingModal extends Modal {
|
||||
private plugin: HandwritingPlugin;
|
||||
private embedId: string;
|
||||
private svgPath: string;
|
||||
private sourcePath: string;
|
||||
private canvas: DrawingCanvas | null = null;
|
||||
private saveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
constructor(app: App, plugin: HandwritingPlugin, embedId: string, svgPath: string, sourcePath: string) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.embedId = embedId;
|
||||
this.svgPath = svgPath;
|
||||
this.sourcePath = sourcePath;
|
||||
this.modalEl.addClass('hwm_modal');
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
this.contentEl.addClass('hwm_editor-view');
|
||||
await this.buildEditor();
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
if (this.canvas) {
|
||||
await this.saveSvg();
|
||||
this.canvas.destroy();
|
||||
this.canvas = null;
|
||||
}
|
||||
if (this.saveTimer) clearTimeout(this.saveTimer);
|
||||
}
|
||||
|
||||
private async buildEditor() {
|
||||
const el = this.contentEl;
|
||||
const isMobile = Platform.isMobile;
|
||||
const isDark = this.plugin.settings.bgMode === 'dark';
|
||||
const bgColor = getEffectiveBgColor(this.plugin.settings);
|
||||
const lineColor = getEffectiveLineColor(this.plugin.settings);
|
||||
el.style.backgroundColor = bgColor;
|
||||
|
||||
// Top bar con toolbar (nessun backBtn: il Modal ha già la X nativa)
|
||||
const topbar = el.createDiv({ cls: 'hwm_editor-topbar' });
|
||||
if (isDark) topbar.classList.add('hwm_editor-topbar--dark');
|
||||
|
||||
const toolbar = topbar.createDiv({ cls: 'hwm_toolbar hwm_editor-toolbar' });
|
||||
if (isDark) toolbar.classList.add('hwm_toolbar--dark');
|
||||
|
||||
if (isMobile) {
|
||||
toolbar.classList.add('hwm_toolbar--compact');
|
||||
const toggleBtn = this.mkBtn(toolbar, 'chevron-down', 'Mostra tutti');
|
||||
toggleBtn.classList.add('hwm_toggle-btn');
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
const compact = toolbar.classList.toggle('hwm_toolbar--compact');
|
||||
updateColorSizes(compact);
|
||||
toggleBtn.innerHTML = ICONS[compact ? 'chevron-down' : 'chevron-up'] ?? '';
|
||||
});
|
||||
}
|
||||
|
||||
const penBtn = this.mkBtn(toolbar, 'pencil', 'Penna');
|
||||
penBtn.classList.add('hwm_active', 'hwm_pen-btn');
|
||||
const eraserBtn = this.mkBtn(toolbar, 'eraser', 'Gomma');
|
||||
eraserBtn.classList.add('hwm_eraser-btn');
|
||||
toolbar.createDiv({ cls: 'hwm_separator' });
|
||||
|
||||
const colors = isDark
|
||||
? ['#ffffff', '#60a5fa', '#f87171', '#4ade80']
|
||||
: ['#000000', '#1e40af', '#dc2626', '#16a34a'];
|
||||
const colorWrap = toolbar.createDiv({ cls: 'hwm_colors' });
|
||||
const colorBtns: HTMLElement[] = [];
|
||||
for (const c of colors) {
|
||||
const btn = colorWrap.createEl('div', { cls: 'hwm_color-btn', attr: { title: c, role: 'button', tabindex: '0' } });
|
||||
btn.style.backgroundColor = c;
|
||||
for (const [k, v] of Object.entries({
|
||||
width: '22px', height: '22px', 'min-width': '22px', 'min-height': '22px',
|
||||
'border-radius': '50%', 'box-sizing': 'border-box', 'flex-shrink': '0'
|
||||
})) btn.style.setProperty(k, v, 'important');
|
||||
if (c === colors[0]) btn.classList.add('hwm_active');
|
||||
colorBtns.push(btn);
|
||||
}
|
||||
toolbar.createDiv({ cls: 'hwm_separator' });
|
||||
|
||||
const updateColorSizes = (compact: boolean) => {
|
||||
colorBtns.forEach(b => {
|
||||
const sz = (!compact || b.classList.contains('hwm_active')) ? '22px' : '0';
|
||||
b.style.setProperty('min-width', sz, 'important');
|
||||
b.style.setProperty('min-height', sz, 'important');
|
||||
});
|
||||
};
|
||||
if (isMobile) updateColorSizes(true);
|
||||
|
||||
const undoBtn = this.mkBtn(toolbar, 'rotate-ccw', 'Annulla');
|
||||
undoBtn.classList.add('hwm_undo-btn');
|
||||
const redoBtn = this.mkBtn(toolbar, 'rotate-cw', 'Ripristina');
|
||||
redoBtn.classList.add('hwm_redo-btn');
|
||||
const clearBtn = this.mkBtn(toolbar, 'trash', 'Cancella tutto');
|
||||
clearBtn.classList.add('hwm_clear-btn');
|
||||
toolbar.createDiv({ cls: 'hwm_separator' });
|
||||
|
||||
const convertBtn = this.mkBtn(toolbar, 'file-text', 'Converti in Markdown');
|
||||
convertBtn.classList.add('hwm_convert-btn');
|
||||
const saveBtn = this.mkBtn(toolbar, 'save', 'Salva');
|
||||
saveBtn.classList.add('hwm_save-btn');
|
||||
const deleteBtn = this.mkBtn(toolbar, 'x', 'Elimina riquadro');
|
||||
deleteBtn.classList.add('hwm_delete-btn');
|
||||
|
||||
const scrollWrap = el.createDiv({ cls: 'hwm_editor-scroll' });
|
||||
const canvasWrap = scrollWrap.createDiv({ cls: 'hwm_canvas-wrap' });
|
||||
|
||||
const { strokes, canvasHeight: savedH } = await this.loadStrokes();
|
||||
const { canvasWidth, canvasHeight } = this.plugin.settings;
|
||||
const h = savedH ?? canvasHeight;
|
||||
const debugFn = this.plugin.settings.debugMode ? (msg: string) => new Notice(msg, 3000) : null;
|
||||
|
||||
this.canvas = new DrawingCanvas(canvasWrap, canvasWidth, h, canvasHeight, isMobile, debugFn);
|
||||
this.canvas.setBackground(bgColor, lineColor);
|
||||
this.canvas.setColor(colors[0]!);
|
||||
if (isMobile) this.canvas.allowFingerScroll(scrollWrap);
|
||||
|
||||
if (strokes.length > 0) {
|
||||
const remapped = strokes.map(s => ({ ...s, color: remapStrokeColor(s.color, this.plugin.settings.bgMode) }));
|
||||
this.canvas.loadStrokes(remapped);
|
||||
}
|
||||
|
||||
const handle = scrollWrap.createDiv({ cls: 'hwm_resize-handle hwm_resize-handle--disabled' });
|
||||
handle.createEl('span', { text: '⋯' });
|
||||
if (isDark) { handle.style.background = '#2a2a2a'; handle.style.borderTopColor = '#444'; handle.style.color = '#888'; }
|
||||
|
||||
this.canvas.onResize(() => { scrollWrap.scrollTop = scrollWrap.scrollHeight; });
|
||||
|
||||
const cv = this.canvas;
|
||||
penBtn.addEventListener('click', () => { cv.setMode('pen'); penBtn.classList.add('hwm_active'); eraserBtn.classList.remove('hwm_active'); });
|
||||
eraserBtn.addEventListener('click', () => { cv.setMode('eraser'); eraserBtn.classList.add('hwm_active'); penBtn.classList.remove('hwm_active'); });
|
||||
for (let i = 0; i < colorBtns.length; i++) {
|
||||
colorBtns[i]!.addEventListener('click', () => {
|
||||
colorBtns.forEach(b => b.classList.remove('hwm_active'));
|
||||
colorBtns[i]!.classList.add('hwm_active');
|
||||
cv.setColor(colors[i]!);
|
||||
if (isMobile) updateColorSizes(toolbar.classList.contains('hwm_toolbar--compact'));
|
||||
});
|
||||
}
|
||||
undoBtn.addEventListener('click', () => cv.undo());
|
||||
redoBtn.addEventListener('click', () => cv.redo());
|
||||
clearBtn.addEventListener('click', () => cv.clear());
|
||||
convertBtn.addEventListener('click', () => this.doConvert());
|
||||
saveBtn.addEventListener('click', async () => { await this.saveSvg(); new Notice('Salvato'); });
|
||||
deleteBtn.addEventListener('click', () => this.doDelete());
|
||||
|
||||
cv.onChange(() => {
|
||||
if (this.saveTimer) clearTimeout(this.saveTimer);
|
||||
this.saveTimer = setTimeout(() => this.saveSvg(), 2000);
|
||||
});
|
||||
}
|
||||
|
||||
private async loadStrokes(): Promise<{ strokes: Stroke[]; canvasHeight: number | null }> {
|
||||
const file = this.app.vault.getAbstractFileByPath(this.svgPath);
|
||||
if (file instanceof TFile) {
|
||||
const content = await this.app.vault.read(file);
|
||||
const m = content.match(/viewBox="0 0 \d+ (\d+)"/);
|
||||
return { strokes: parseSvgStrokes(content), canvasHeight: m ? parseInt(m[1] ?? '0') : null };
|
||||
}
|
||||
return { strokes: [], canvasHeight: null };
|
||||
}
|
||||
|
||||
private async saveSvg() {
|
||||
if (!this.canvas) return;
|
||||
const svg = strokesToSvg(this.canvas.getStrokes(), this.canvas.getWidth(), this.canvas.getHeight(),
|
||||
this.canvas.getBgColor(), this.canvas.getLineColor());
|
||||
const folder = this.svgPath.substring(0, this.svgPath.lastIndexOf('/'));
|
||||
if (folder && !this.app.vault.getAbstractFileByPath(folder)) await this.app.vault.createFolder(folder);
|
||||
const existing = this.app.vault.getAbstractFileByPath(this.svgPath);
|
||||
if (existing instanceof TFile) { await this.app.vault.modify(existing, svg); }
|
||||
else { await this.app.vault.create(this.svgPath, svg); }
|
||||
this.plugin.refreshPreview(this.embedId, svg);
|
||||
}
|
||||
|
||||
private async doConvert() {
|
||||
if (!this.canvas || this.canvas.getStrokes().length === 0) { new Notice('Nessun tratto da convertire'); return; }
|
||||
try {
|
||||
new Notice('Riconoscimento in corso…');
|
||||
const svg = strokesToSvg(this.canvas.getStrokes(), this.canvas.getWidth(), this.canvas.getHeight(),
|
||||
this.canvas.getBgColor(), this.canvas.getLineColor());
|
||||
const svgEl = new DOMParser().parseFromString(svg, 'image/svg+xml').documentElement as unknown as SVGElement;
|
||||
const base64 = await this.svgToPng(svgEl);
|
||||
const recognizer = getRecognizer(this.plugin.settings.geminiApiKey, this.plugin.settings.ocrLanguages);
|
||||
const rawText = await recognizer.recognize(base64);
|
||||
if (!rawText.trim()) { new Notice('Nessun testo riconosciuto'); return; }
|
||||
const markdown = parseMarkdown(rawText);
|
||||
await this.archiveSvg();
|
||||
await this.replaceCodeBlock(markdown);
|
||||
this.canvas.destroy(); this.canvas = null;
|
||||
this.close();
|
||||
new Notice('Conversione completata!');
|
||||
} catch (e: unknown) { new Notice('Errore OCR: ' + (e instanceof Error ? e.message : String(e))); }
|
||||
}
|
||||
|
||||
private async doDelete() {
|
||||
if (!confirm('Eliminare questo riquadro handwriting e il file SVG associato?')) return;
|
||||
if (this.canvas) { this.canvas.destroy(); this.canvas = null; }
|
||||
await this.removeCodeBlock();
|
||||
const svgFile = this.app.vault.getAbstractFileByPath(this.svgPath);
|
||||
if (svgFile instanceof TFile) await this.app.vault.delete(svgFile);
|
||||
this.close();
|
||||
new Notice('Riquadro eliminato');
|
||||
}
|
||||
|
||||
private async archiveSvg() {
|
||||
const svgFile = this.app.vault.getAbstractFileByPath(this.svgPath);
|
||||
if (!(svgFile instanceof TFile)) return;
|
||||
const now = new Date(); const pad = (n: number) => String(n).padStart(2, '0');
|
||||
const ts = `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
|
||||
const dest = `${this.plugin.settings.svgFolder}/_converted`;
|
||||
if (!this.app.vault.getAbstractFileByPath(dest)) await this.app.vault.createFolder(dest);
|
||||
await this.app.vault.rename(svgFile, `${dest}/${ts}.svg`);
|
||||
}
|
||||
|
||||
private codeBlockRegex(): RegExp {
|
||||
const esc = this.embedId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
return new RegExp('\\n?```handwriting\\n.*?"id"\\s*:\\s*"' + esc + '".*?\\n```\\n?', 's');
|
||||
}
|
||||
|
||||
private async replaceCodeBlock(markdown: string) {
|
||||
const mdFile = this.app.vault.getAbstractFileByPath(this.sourcePath);
|
||||
if (!(mdFile instanceof TFile)) { new Notice('File markdown non trovato'); return; }
|
||||
const content = await this.app.vault.read(mdFile);
|
||||
const updated = content.replace(this.codeBlockRegex(), '\n' + markdown + '\n');
|
||||
if (updated !== content) await this.app.vault.modify(mdFile, updated);
|
||||
}
|
||||
|
||||
private async removeCodeBlock() {
|
||||
const mdFile = this.app.vault.getAbstractFileByPath(this.sourcePath);
|
||||
if (!(mdFile instanceof TFile)) { new Notice('File markdown non trovato'); return; }
|
||||
const content = await this.app.vault.read(mdFile);
|
||||
const updated = content.replace(this.codeBlockRegex(), '\n');
|
||||
if (updated !== content) await this.app.vault.modify(mdFile, updated);
|
||||
}
|
||||
|
||||
private mkBtn(parent: HTMLElement, icon: string, title: string): HTMLElement {
|
||||
const btn = parent.createEl('button', { cls: 'hwm_btn', attr: { title } });
|
||||
btn.innerHTML = ICONS[icon] ?? '';
|
||||
return btn;
|
||||
}
|
||||
|
||||
private svgToPng(svgElement: SVGElement): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cvs = document.createElement('canvas');
|
||||
const ctx = cvs.getContext('2d')!;
|
||||
const img = new Image();
|
||||
const blob = new Blob([new XMLSerializer().serializeToString(svgElement)], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
img.onload = () => { cvs.width = img.width; cvs.height = img.height; ctx.drawImage(img, 0, 0); URL.revokeObjectURL(url); resolve(cvs.toDataURL('image/png').split(',')[1]!); };
|
||||
img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('SVG → PNG fallito')); };
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -10,13 +10,25 @@
|
|||
import { Plugin } from 'obsidian';
|
||||
import { DEFAULT_SETTINGS, HandwritingSettings, HandwritingSettingTab } from './settings';
|
||||
import { registerEmbed, insertHandwritingBlock } from './embed';
|
||||
import { VIEW_TYPE_HANDWRITING, DrawingEditorView } from './editor-view';
|
||||
|
||||
export default class HandwritingPlugin extends Plugin {
|
||||
settings: HandwritingSettings;
|
||||
|
||||
// Mappa di callback per aggiornare le preview inline quando l'editor tab salva
|
||||
public previewCallbacks = new Map<string, (svgContent: string) => void>();
|
||||
|
||||
// Invocato dall'editor tab dopo ogni salvataggio per aggiornare la preview inline
|
||||
refreshPreview(id: string, svgContent: string) {
|
||||
this.previewCallbacks.get(id)?.(svgContent);
|
||||
}
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
// Registra la vista editor (tab dedicata per il disegno)
|
||||
this.registerView(VIEW_TYPE_HANDWRITING, (leaf) => new DrawingEditorView(leaf, this));
|
||||
|
||||
// Registra il code block processor per ```handwriting
|
||||
registerEmbed(this);
|
||||
|
||||
|
|
@ -24,14 +36,14 @@ export default class HandwritingPlugin extends Plugin {
|
|||
this.addCommand({
|
||||
id: 'insert-handwriting',
|
||||
name: 'Insert handwriting block',
|
||||
editorCallback: () => {
|
||||
insertHandwritingBlock(this);
|
||||
editorCallback: async () => {
|
||||
await insertHandwritingBlock(this);
|
||||
}
|
||||
});
|
||||
|
||||
// Icona nella ribbon (sidebar sinistra)
|
||||
this.addRibbonIcon('pencil', 'Insert handwriting', () => {
|
||||
insertHandwritingBlock(this);
|
||||
this.addRibbonIcon('pencil', 'Insert handwriting', async () => {
|
||||
await insertHandwritingBlock(this);
|
||||
});
|
||||
|
||||
// Tab impostazioni
|
||||
|
|
|
|||
|
|
@ -12,6 +12,26 @@
|
|||
margin: 8px 0;
|
||||
/* Sfondo SEMPRE bianco (anche in dark mode) */
|
||||
background: #ffffff;
|
||||
/* Riabilita pointer-events (i wrapper CM6 sopra hanno pointer-events: none
|
||||
per non bloccare l'hit-test dell'handwriting Android) */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Variante wiki ![[svg]]:
|
||||
La classe hwm_container viene aggiunta DIRETTAMENTE allo span
|
||||
.internal-embed di Obsidian (nessun wrapper esterno, per non
|
||||
spostare i nodi CM6 e causare crash del virtual DOM).
|
||||
L'immagine è già renderizzata da Obsidian; noi aggiungiamo solo
|
||||
i bottoni come figli dello span. */
|
||||
.hwm_container--wiki {
|
||||
background: transparent; /* l'SVG ha già il suo sfondo */
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.hwm_container--wiki img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* --- Toolbar in alto a destra --- */
|
||||
|
|
@ -257,3 +277,197 @@
|
|||
border-color: var(--interactive-accent) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
INLINE PREVIEW — Vista nel documento
|
||||
======================================== */
|
||||
|
||||
/* --- Bottoni inline (X, converti, comprimi) ---
|
||||
Posizionati in alto a sinistra per non coprire il bottone </> di Obsidian */
|
||||
.hwm_inline-buttons {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 3px;
|
||||
border-radius: 6px;
|
||||
background: rgba(240, 240, 240, 0.9);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.hwm_inline-buttons--dark {
|
||||
background: rgba(40, 40, 40, 0.95);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.hwm_inline-buttons--dark .hwm_btn {
|
||||
color: #bbb;
|
||||
}
|
||||
.hwm_inline-buttons--dark .hwm_btn:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* --- Bottone comprimi: rotazione 180° quando compresso --- */
|
||||
.hwm_collapse-btn {
|
||||
transition: transform 0.3s ease, max-width 0.2s ease,
|
||||
opacity 0.2s ease, margin 0.2s ease, padding 0.2s ease;
|
||||
}
|
||||
.hwm_collapse-btn.hwm_rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* --- Preview SVG inline --- */
|
||||
.hwm_inline-preview {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
/* Transizione per l'espansione/compressione */
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
.hwm_inline-preview.hwm_collapsed {
|
||||
/* max-height impostato via JS */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Preview SVG via background-image (nessun <img> nel DOM per non
|
||||
interferire con l'handwriting Android dentro cm-content) */
|
||||
.hwm_preview-bg {
|
||||
width: 100%;
|
||||
height: 0; /* l'altezza è data da padding-bottom (aspect ratio) */
|
||||
background-size: 100% auto;
|
||||
background-repeat: no-repeat;
|
||||
background-position: top left;
|
||||
pointer-events: none; /* il click è gestito dal div padre */
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
PANNELLO PORTALE — Fuori da cm-content
|
||||
Contiene tutti i bottoni del formato wiki.
|
||||
Vive in document.body → zero impatto su
|
||||
cm-content → non rompe handwriting Android
|
||||
======================================== */
|
||||
|
||||
/* Pannello floating con tutti i bottoni (nuovo formato ![[svg]]) */
|
||||
.hwm_portal-panel {
|
||||
position: fixed; /* coordinate viewport, aggiornate via RAF */
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0; /* spacing via margin-right sui .hwm_btn */
|
||||
padding: 3px 6px;
|
||||
border-radius: 6px;
|
||||
background: rgba(240, 240, 240, 0.95);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Bottone cerchio singolo (formato legacy) */
|
||||
.hwm_portal-btn {
|
||||
position: fixed; /* coordinate viewport, aggiornate via JS */
|
||||
z-index: 100;
|
||||
/* Cerchio perfetto: dimensioni fisse con !important per sovrascrivere
|
||||
gli stili globali di Obsidian sui button che aggiungono padding */
|
||||
width: 36px !important;
|
||||
height: 36px !important;
|
||||
min-width: 36px !important;
|
||||
min-height: 36px !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 50% !important;
|
||||
border: none !important;
|
||||
background: var(--interactive-accent) !important;
|
||||
color: #ffffff !important;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden; /* evita che l'SVG allarghi il bottone */
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
.hwm_portal-btn svg {
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
flex-shrink: 0;
|
||||
stroke: #ffffff !important; /* forza icona bianca indipendentemente da currentColor */
|
||||
}
|
||||
.hwm_portal-btn:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
DRAWING MODAL — Editor come overlay
|
||||
======================================== */
|
||||
|
||||
.hwm_modal {
|
||||
width: 95vw !important;
|
||||
max-width: 95vw !important;
|
||||
height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.hwm_modal .modal-content {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
EDITOR VIEW — Tab dedicata al disegno
|
||||
======================================== */
|
||||
|
||||
/* Container principale dell'editor view */
|
||||
.hwm_editor-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* --- Top bar: ← a sinistra | toolbar a destra --- */
|
||||
.hwm_editor-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 8px;
|
||||
background: rgba(240, 240, 240, 0.95);
|
||||
border-bottom: 1px solid #ddd;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
}
|
||||
.hwm_editor-topbar--dark {
|
||||
background: rgba(40, 40, 40, 0.97);
|
||||
border-bottom-color: #444;
|
||||
}
|
||||
|
||||
/* Bottone ← (indietro, a sinistra) */
|
||||
.hwm_back-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Toolbar dentro l'editor view: posizione statica, allineata a destra */
|
||||
.hwm_editor-toolbar {
|
||||
position: static !important;
|
||||
top: auto !important;
|
||||
right: auto !important;
|
||||
box-shadow: none !important;
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* --- Scroll container per il canvas nell'editor --- */
|
||||
.hwm_editor-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* --- Resize handle disabilitato (visibile ma non interattivo) --- */
|
||||
.hwm_resize-handle--disabled {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
.hwm_resize-handle--disabled:hover {
|
||||
background: #f5f5f5; /* annulla l'effetto hover */
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue