mirror of
https://github.com/gabriele-cusato/HandTranscriptMd.git
synced 2026-07-22 06:14:06 +00:00
aggiunto layer di caricamento quando faccio partire la conversione
This commit is contained in:
parent
e4f6059fcc
commit
e02f936401
4 changed files with 215 additions and 32 deletions
|
|
@ -401,27 +401,35 @@ export class DrawingEditorView extends ItemView {
|
|||
await saveSvgToDisk(this.canvas, this.svgPath, this.embedId, this.plugin);
|
||||
}
|
||||
|
||||
private async doConvert() {
|
||||
private async doConvert() {
|
||||
if (!this.canvas || this.canvas.getStrokes().length === 0) {
|
||||
new Notice(t('error_no_strokes')); return;
|
||||
}
|
||||
// Overlay a tutto schermo sull'editor: spinner + blocco interazione
|
||||
const overlay = this.contentEl.createDiv({ cls: 'hwm_confirm-overlay hwm_convert-overlay--editor' });
|
||||
overlay.createDiv({ cls: 'hwm_spinner' });
|
||||
try {
|
||||
new Notice(t('notice_recognizing'));
|
||||
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 svgToBase64Png(svgEl);
|
||||
const recognizer = getRecognizer(this.plugin.settings.geminiApiKey, this.plugin.settings.ocrLanguages);
|
||||
const rawText = await recognizer.recognize(base64);
|
||||
if (!rawText.trim()) { new Notice(t('error_no_text')); return; }
|
||||
if (!rawText.trim()) throw new Error(t('error_no_text'));
|
||||
const markdown = parseHandwritingToMarkdown(rawText);
|
||||
await archiveSvgFile(this.svgPath, this.plugin);
|
||||
await replaceInMdFile(this.sourcePath, this.svgPath, this.embedId, '\n' + markdown + '\n', this.plugin);
|
||||
overlay.remove();
|
||||
this.canvas.destroy(); this.canvas = null;
|
||||
this.leaf.detach();
|
||||
new Notice(t('notice_converted'));
|
||||
} catch (e: unknown) {
|
||||
new Notice(t('error_ocr') + (e instanceof Error ? e.message : String(e)));
|
||||
// Errore: sostituisce lo spinner con messaggio + OK
|
||||
overlay.empty();
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
overlay.createEl('p', { text: msg, cls: 'hwm_convert-error-msg' });
|
||||
const okBtn = overlay.createEl('button', { text: 'OK', cls: 'hwm_convert-ok-btn mod-warning' });
|
||||
okBtn.addEventListener('click', () => overlay.remove(), { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -523,24 +531,34 @@ export class DrawingModal extends Modal {
|
|||
await saveSvgToDisk(this.canvas, this.svgPath, this.embedId, this.plugin);
|
||||
}
|
||||
|
||||
private async doConvert() {
|
||||
private async doConvert() {
|
||||
if (!this.canvas || this.canvas.getStrokes().length === 0) { new Notice(t('error_no_strokes')); return; }
|
||||
// Overlay a tutto schermo sul modal: spinner + blocco interazione
|
||||
const overlay = this.contentEl.createDiv({ cls: 'hwm_confirm-overlay hwm_convert-overlay--editor' });
|
||||
overlay.createDiv({ cls: 'hwm_spinner' });
|
||||
try {
|
||||
new Notice(t('notice_recognizing'));
|
||||
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 svgToBase64Png(svgEl);
|
||||
const recognizer = getRecognizer(this.plugin.settings.geminiApiKey, this.plugin.settings.ocrLanguages);
|
||||
const rawText = await recognizer.recognize(base64);
|
||||
if (!rawText.trim()) { new Notice(t('error_no_text')); return; }
|
||||
if (!rawText.trim()) throw new Error(t('error_no_text'));
|
||||
const markdown = parseHandwritingToMarkdown(rawText);
|
||||
await archiveSvgFile(this.svgPath, this.plugin);
|
||||
await replaceInMdFile(this.sourcePath, this.svgPath, this.embedId, '\n' + markdown + '\n', this.plugin);
|
||||
overlay.remove();
|
||||
this.canvas.destroy(); this.canvas = null;
|
||||
this.close();
|
||||
new Notice(t('notice_converted'));
|
||||
} catch (e: unknown) { new Notice(t('error_ocr') + (e instanceof Error ? e.message : String(e))); }
|
||||
} catch (e: unknown) {
|
||||
// Errore: sostituisce lo spinner con messaggio + OK
|
||||
overlay.empty();
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
overlay.createEl('p', { text: msg, cls: 'hwm_convert-error-msg' });
|
||||
const okBtn = overlay.createEl('button', { text: 'OK', cls: 'hwm_convert-ok-btn mod-warning' });
|
||||
okBtn.addEventListener('click', () => overlay.remove(), { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay di conferma inline: nessun Modal annidato → nessun furto di focus
|
||||
|
|
|
|||
|
|
@ -574,6 +574,7 @@ function createPortalPanel(
|
|||
let isExpanded = true;
|
||||
// Flag per nascondere il pannello quando il modal (Desktop) è aperto
|
||||
let modalOpen = false;
|
||||
let isConverting = false; // true mentre OCR e' in corso (o errore non ancora confermato)
|
||||
|
||||
// Lo span diventa position: relative per ancorarvi il pannello (position: absolute).
|
||||
container.style.position = 'relative';
|
||||
|
|
@ -699,21 +700,55 @@ function createPortalPanel(
|
|||
collapseBtn.title = t('btn_expand');
|
||||
collapseBtn.setAttribute('data-hwm-key', 'btn_expand');
|
||||
};
|
||||
// Carica SVG e chiama doConvertWiki (che lancia eccezione in caso di errore)
|
||||
// Overlay di conversione: spinner mentre OCR e' in corso,
|
||||
// poi errore + OK se Gemini fallisce.
|
||||
// Nasconde il pannello portale (come modalOpen) per bloccare tutti i click.
|
||||
const showConvertOverlay = (): HTMLElement => {
|
||||
// Nasconde il pannello: stessa logica di modalOpen (evita click sui bottoni durante OCR)
|
||||
panel.style.display = 'none';
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'hwm_convert-overlay';
|
||||
const spinner = document.createElement('div');
|
||||
spinner.className = 'hwm_spinner';
|
||||
overlay.appendChild(spinner);
|
||||
container.appendChild(overlay);
|
||||
return overlay;
|
||||
};
|
||||
|
||||
// Rimuove overlay e ripristina il pannello portale
|
||||
const removeConvertOverlay = (overlay: HTMLElement) => {
|
||||
overlay.remove();
|
||||
if (container.isConnected) panel.style.display = '';
|
||||
isConverting = false;
|
||||
};
|
||||
|
||||
// Avvia la conversione OCR con overlay. Non lancia eccezioni:
|
||||
// gli errori vengono mostrati nell'overlay stesso con pulsante OK.
|
||||
const doConvertAction = async () => {
|
||||
const { strokes, svgContent } = await loadSvgData(svgPath, plugin);
|
||||
if (!svgContent || strokes.length === 0) throw new Error('Nessun tratto da convertire');
|
||||
await doConvertWiki(svgContent, svgPath, sourcePath, plugin);
|
||||
if (isConverting) return; // skip se gia' in corso (usato anche da 'converti tutti')
|
||||
isConverting = true;
|
||||
const overlay = showConvertOverlay();
|
||||
try {
|
||||
const { strokes, svgContent } = await loadSvgData(svgPath, plugin);
|
||||
if (!svgContent || strokes.length === 0) throw new Error(t('error_no_strokes'));
|
||||
await doConvertWiki(svgContent, svgPath, sourcePath, plugin);
|
||||
// Successo: rimuove overlay e ripristina pannello
|
||||
removeConvertOverlay(overlay);
|
||||
} catch (e: unknown) {
|
||||
// Errore: sostituisce lo spinner con messaggio + OK
|
||||
overlay.empty();
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
overlay.createEl('p', { text: msg, cls: 'hwm_convert-error-msg' });
|
||||
const okBtn = overlay.createEl('button', { text: 'OK', cls: 'hwm_convert-ok-btn mod-warning' });
|
||||
// L'overlay resta visibile finche' l'utente non clicca OK
|
||||
okBtn.addEventListener('click', () => removeConvertOverlay(overlay), { once: true });
|
||||
}
|
||||
};
|
||||
|
||||
collapseBtn.addEventListener('click', () => {
|
||||
if (isExpanded) doCollapse(); else doExpand();
|
||||
});
|
||||
convertBtn.addEventListener('click', async () => {
|
||||
// Bottone singolo: mostra Notice in caso di errore senza propagare
|
||||
try { await doConvertAction(); }
|
||||
catch (e: unknown) { new Notice('Errore OCR: ' + (e instanceof Error ? e.message : String(e))); }
|
||||
});
|
||||
convertBtn.addEventListener('click', () => doConvertAction());
|
||||
|
||||
// Registra le azioni nel plugin per il menu "⋮ Espandi/Collassa/Converti tutti"
|
||||
plugin.embedActions.set(embedId, { expand: doExpand, collapse: doCollapse, convert: doConvertAction, container, sourcePath });
|
||||
|
|
|
|||
|
|
@ -691,6 +691,55 @@
|
|||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Overlay di conversione OCR: copre l'SVG mentre Gemini elabora.
|
||||
Stessa struttura di hwm_confirm-overlay ma con spinner animato
|
||||
e fase di errore con messaggio + pulsante OK. */
|
||||
.hwm_convert-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
z-index: 100;
|
||||
border-radius: inherit;
|
||||
pointer-events: auto !important; /* blocca click sull'SVG sottostante */
|
||||
}
|
||||
/* Rotellina di caricamento */
|
||||
.hwm_spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: hwm_spin 0.75s linear infinite;
|
||||
}
|
||||
@keyframes hwm_spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
/* Messaggio di errore mostrato al posto dello spinner */
|
||||
.hwm_convert-error-msg {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
max-width: 80%;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
/* Pulsante OK per chiudere l'overlay di errore */
|
||||
.hwm_convert-ok-btn {
|
||||
pointer-events: auto !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* Variante editor: copre l'intero modal/tab con z-index alto
|
||||
per stare sopra toolbar e canvas */
|
||||
.hwm_convert-overlay--editor {
|
||||
z-index: 1000;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Wrapper per il collapse: contiene l'<img> e gestisce il clip.
|
||||
Animiamo solo il wrapper, non il container span, così il ResizeObserver
|
||||
di Obsidian Mobile non si attiva e non re-imposta le dimensioni dell'img. */
|
||||
|
|
|
|||
111
README.md
111
README.md
|
|
@ -86,27 +86,108 @@ The plugin uses **Google Gemini** to recognize handwritten text and converts it
|
|||
|
||||
#### Supported Keywords
|
||||
|
||||
Write these keywords in your drawing to produce structured Markdown output. All keywords start with `//` and are **case-insensitive** (`//h1` = `//H1`).
|
||||
Write these keywords in your drawing to produce structured Markdown output. All keywords start with `//` and are **case-insensitive** (`//list` = `//LIST`). The colon after the keyword name is **optional** (`//H1 Title` and `//H1: Title` both work).
|
||||
|
||||
| Keyword | Syntax | Output |
|
||||
|---------|--------|--------|
|
||||
| `//h1` | `//h1 My Title` | `# My Title` |
|
||||
| `//h2` | `//h2 Section` | `## Section` |
|
||||
| `//h3` | `//h3 Sub` | `### Sub` |
|
||||
| `//ul` | `//ul Item` | `- Item` |
|
||||
| `//ol` | `//ol Item` | `1. Item` |
|
||||
| `//todo` | `//todo Task` | `- [ ] Task` |
|
||||
| `//done` | `//done Task` | `- [x] Task` |
|
||||
| `//quote` | `//quote Text` | `> Text` |
|
||||
| `//code` | `//code snippet` | `` `snippet` `` |
|
||||
| `//hr` | `//hr` | `---` |
|
||||
| `//bold` | `//bold text` | `**text**` |
|
||||
| `//italic` | `//italic text` | `*text*` |
|
||||
| `//highlight` | `//highlight text` | `==text==` |
|
||||
| `//TABLE` ... `//TABLE` | rows between two `//TABLE` markers | Markdown table |
|
||||
| `//H1` | `//H1 My Title` | `# My Title` |
|
||||
| `//H2` | `//H2 Section` | `## Section` |
|
||||
| `//H3` | `//H3 Sub` | `### Sub` |
|
||||
| `//H4` | `//H4 Sub` | `#### Sub` |
|
||||
| `//LIST` | `//LIST item1, item2, item3` | bullet list |
|
||||
| `//NUMLIST` | `//NUMLIST item1, item2` | numbered list (starts at 1) |
|
||||
| `//NUMLIST` (offset) | `//NUMLIST 3 item1, item2` | numbered list starting at 3 |
|
||||
| `//CHECK` | `//CHECK task1, task2` | checklist (all unchecked) |
|
||||
| `//CHECK` (mixed) | `//CHECK x done, pending, x also done` | checklist with checked/unchecked items |
|
||||
| `//QUOTE` | `//QUOTE Text` | `> Text` |
|
||||
| `//NOTE` | `//NOTE Title` | Obsidian callout `[!NOTE]` |
|
||||
| `//WARN` | `//WARN Title` | Obsidian callout `[!WARNING]` |
|
||||
| `//TIP` | `//TIP Title` | Obsidian callout `[!TIP]` |
|
||||
| `//INFO` | `//INFO Title` | Obsidian callout `[!INFO]` |
|
||||
| `//ERROR` | `//ERROR Title` | Obsidian callout `[!ERROR]` |
|
||||
| `//IMPORTANT` | `//IMPORTANT Title` | Obsidian callout `[!IMPORTANT]` |
|
||||
| `//CODE` | `//CODE snippet` | `` `snippet` `` (inline code) |
|
||||
| `//CODEBLOCK` | `//CODEBLOCK js` + lines + blank line | fenced code block |
|
||||
| `//B` / `//BOLD` | `//BOLD text` | `**text**` |
|
||||
| `//I` | `//I text` | `*text*` |
|
||||
| `//BI` | `//BI text` | `***text***` |
|
||||
| `//S` / `//STRIKE` | `//S text` | `~~text~~` |
|
||||
| `//HL` | `//HL text` | `==text==` (highlight) |
|
||||
| `//LINK` | `//LINK label, url` | `[label](url)` |
|
||||
| `//IMG` | `//IMG alt, url` | `` |
|
||||
| `//TABLE` | `//TABLE Col1, Col2` + rows + `//TABLE` | Markdown table |
|
||||
| `//HR` / `//SEP` | `//HR` | `---` |
|
||||
| `//FN` | `//FN footnote text` | `[^1]: footnote text` (auto-numbered) |
|
||||
| `//MATH` | `//MATH x^2` | `$x^2$` (inline math) |
|
||||
| `//MATHBLOCK` | `//MATHBLOCK` + lines + blank line | `$$...$$` math block |
|
||||
| `//TAG` | `//TAG my tag` | `#my_tag` |
|
||||
| `//DATE` | `//DATE` | today's date (YYYY-MM-DD) |
|
||||
| `//TIME` | `//TIME` | current time (HH:MM) |
|
||||
| `//DATETIME` | `//DATETIME` | date + time |
|
||||
| `//INDENT` | `//INDENT text` | text indented by 2 spaces |
|
||||
|
||||
Plain text lines (without a `//` keyword) are inserted as-is.
|
||||
|
||||
---
|
||||
|
||||
#### Multi-line Continuation
|
||||
|
||||
Any keyword that accepts a comma-separated list (`//LIST`, `//NUMLIST`, `//CHECK`, `//TABLE` rows) supports **wrapping across lines**: if a line ends with a comma, the next line is automatically treated as a continuation.
|
||||
|
||||
```
|
||||
//LIST groceries, milk, bread,
|
||||
butter, eggs
|
||||
```
|
||||
Output:
|
||||
```markdown
|
||||
- groceries
|
||||
- milk
|
||||
- bread
|
||||
- butter
|
||||
- eggs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### CHECK with Mixed States
|
||||
|
||||
Prefix any item with `x` or `X` (with or without brackets) to mark it as already checked:
|
||||
|
||||
```
|
||||
//CHECK x bought milk, prepare slides, x sent email, review PR
|
||||
```
|
||||
Output:
|
||||
```markdown
|
||||
- [x] bought milk
|
||||
- [ ] prepare slides
|
||||
- [x] sent email
|
||||
- [ ] review PR
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Multi-line Callouts
|
||||
|
||||
The text on the keyword line becomes the callout **title**. Any lines that follow (up to the first blank line or next `//` keyword) become the callout **body**:
|
||||
|
||||
```
|
||||
//NOTE Database connection
|
||||
The connection may fail on an unstable network.
|
||||
Always verify the timeout in the settings.
|
||||
|
||||
Normal paragraph — outside the callout.
|
||||
```
|
||||
Output:
|
||||
```markdown
|
||||
> [!NOTE] Database connection
|
||||
> The connection may fail on an unstable network.
|
||||
> Always verify the timeout in the settings.
|
||||
|
||||
Normal paragraph — outside the callout.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
After conversion, the SVG is archived to `_handwriting/_converted/YYYY-MM-DD_HH-MM-SS.svg` and the drawing block is replaced with the generated Markdown.
|
||||
|
||||
---
|
||||
|
|
|
|||
Loading…
Reference in a new issue