diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 7246c59..fbed6ee 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -7,7 +7,9 @@
"WebFetch(domain:css-tricks.com)",
"WebFetch(domain:chromium.googlesource.com)",
"WebFetch(domain:github.com)",
- "WebFetch(domain:developer.chrome.com)"
+ "WebFetch(domain:developer.chrome.com)",
+ "Bash(\"C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe\" --login -c \"cd 'C:/Projects/pluginObsidian/handWrittenMarkdownConverter/HandTranscriptMd' && bash deploy.sh && bash cloudDeploy.sh\" 2>&1)",
+ "Bash(\"C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe\" --login -c \"cd 'C:/Projects/pluginObsidian/handWrittenMarkdownConverter/HandTranscriptMd' && bash deploy.sh\" 2>&1)"
]
}
}
diff --git a/HandTranscriptMd/src/embed.ts b/HandTranscriptMd/src/embed.ts
index a615a6d..ade9374 100644
--- a/HandTranscriptMd/src/embed.ts
+++ b/HandTranscriptMd/src/embed.ts
@@ -339,23 +339,19 @@ async function doConvertWiki(
sourcePath: string,
plugin: HandwritingPlugin
) {
- try {
- new Notice('Riconoscimento in corso…');
- const parser = new DOMParser();
- const svgEl = parser.parseFromString(svgContent, 'image/svg+xml')
- .documentElement as unknown as SVGElement;
- const base64 = await svgToBase64Png(svgEl);
- const recognizer = getRecognizer(plugin.settings.geminiApiKey, plugin.settings.ocrLanguages);
- const rawText = await recognizer.recognize(base64);
- if (!rawText.trim()) { new Notice('Nessun testo riconosciuto'); return; }
-
- const markdown = parseMarkdown(rawText);
- await archiveSvgByPath(svgPath, plugin);
- await replaceWikiEmbedWithMarkdown(svgPath, markdown, sourcePath, plugin);
- new Notice('Conversione completata!');
- } catch (e: unknown) {
- new Notice('Errore OCR: ' + (e instanceof Error ? e.message : String(e)));
- }
+ // Lancia eccezione in caso di errore (il chiamante decide se mostrare Notice o propagare)
+ new Notice('Riconoscimento in corso…');
+ const parser = new DOMParser();
+ const svgEl = parser.parseFromString(svgContent, 'image/svg+xml')
+ .documentElement as unknown as SVGElement;
+ const base64 = await svgToBase64Png(svgEl);
+ const recognizer = getRecognizer(plugin.settings.geminiApiKey, plugin.settings.ocrLanguages);
+ const rawText = await recognizer.recognize(base64);
+ if (!rawText.trim()) throw new Error('Nessun testo riconosciuto');
+ const markdown = parseMarkdown(rawText);
+ await archiveSvgByPath(svgPath, plugin);
+ await replaceWikiEmbedWithMarkdown(svgPath, markdown, sourcePath, plugin);
+ new Notice('Conversione completata!');
}
/* =============================================
@@ -660,14 +656,6 @@ function createPortalPanel(
// --- Bottone converti in Markdown ---
const convertBtn = createPanelBtn(panel, 'file-text', 'Converti in Markdown');
- convertBtn.addEventListener('click', async () => {
- const { strokes, svgContent } = await loadSvgData(svgPath, plugin);
- if (!svgContent || strokes.length === 0) {
- new Notice('Nessun tratto da convertire');
- return;
- }
- await doConvertWiki(svgContent, svgPath, sourcePath, plugin);
- });
// --- Bottone comprimi/espandi ---
// Usa height + overflow:hidden sul container (non max-height sull'
):
@@ -676,20 +664,6 @@ function createPortalPanel(
// anche da compresso (collapsedHeight è sempre >> 6px + altezza pannello).
const collapseBtn = createPanelBtn(panel, 'chevron-up', 'Comprimi');
collapseBtn.classList.add('hwm_collapse-btn');
- collapseBtn.addEventListener('click', () => {
- isExpanded = !isExpanded;
- if (isExpanded) {
- container.style.height = '';
- container.style.overflow = '';
- collapseBtn.classList.remove('hwm_rotated');
- collapseBtn.title = 'Comprimi';
- } else {
- container.style.height = collapsedHeight + 'px';
- container.style.overflow = 'hidden';
- collapseBtn.classList.add('hwm_rotated');
- collapseBtn.title = 'Espandi';
- }
- });
// --- Bottone elimina ---
const deleteBtn = createPanelBtn(panel, 'x', 'Elimina riquadro');
@@ -699,6 +673,41 @@ function createPortalPanel(
await removeWikiEmbed(svgPath, sourcePath, plugin);
});
+ // --- Funzioni condivise: usate dai bottoni e dal menu globale (⋮ Obsidian) ---
+ const doExpand = () => {
+ isExpanded = true;
+ container.style.height = '';
+ container.style.overflow = '';
+ collapseBtn.classList.remove('hwm_rotated');
+ collapseBtn.title = 'Comprimi';
+ };
+ const doCollapse = () => {
+ isExpanded = false;
+ container.style.height = collapsedHeight + 'px';
+ container.style.overflow = 'hidden';
+ collapseBtn.classList.add('hwm_rotated');
+ collapseBtn.title = 'Espandi';
+ };
+ // Carica SVG e chiama doConvertWiki (che lancia eccezione in caso di errore)
+ 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);
+ };
+
+ 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))); }
+ });
+
+ // Registra le azioni nel plugin per il menu "⋮ Espandi/Collassa/Converti tutti"
+ plugin.embedActions.set(embedId, { expand: doExpand, collapse: doCollapse, convert: doConvertAction, container, sourcePath });
+ plugin.register(() => plugin.embedActions.delete(embedId));
+
// Layout-change: su Mobile nasconde il pannello quando la tab editor è aperta
const onLayoutChange = () => {
if (!container.isConnected) {
diff --git a/HandTranscriptMd/src/main.ts b/HandTranscriptMd/src/main.ts
index 051be73..865cec5 100644
--- a/HandTranscriptMd/src/main.ts
+++ b/HandTranscriptMd/src/main.ts
@@ -7,7 +7,7 @@
- Tab impostazioni
============================================= */
-import { Plugin } from 'obsidian';
+import { Plugin, TFile, Notice } from 'obsidian';
import { DEFAULT_SETTINGS, HandwritingSettings, HandwritingSettingTab } from './settings';
import { registerEmbed, insertHandwritingBlock } from './embed';
import { VIEW_TYPE_HANDWRITING, DrawingEditorView } from './editor-view';
@@ -24,6 +24,15 @@ export default class HandwritingPlugin extends Plugin {
// Callback notificate quando l'utente cambia bgMode nelle impostazioni
public bgModeListeners = new Set<(bgMode: string) => void>();
+ // Mappa embedId → azioni (expand/collapse/convert): usata dal menu "⋮" di Obsidian
+ public embedActions = new Map void;
+ collapse: () => void;
+ convert: () => Promise;
+ container: HTMLElement;
+ sourcePath: string;
+ }>();
+
// Invocato dall'editor tab dopo ogni salvataggio per aggiornare la preview inline
refreshPreview(id: string, svgContent: string) {
this.previewCallbacks.get(id)?.(svgContent);
@@ -68,6 +77,67 @@ export default class HandwritingPlugin extends Plugin {
// Tab impostazioni
this.addSettingTab(new HandwritingSettingTab(this.app, this));
+
+ // Voci nel menu "⋮" (tre puntini) di Obsidian per operazioni su tutti i disegni.
+ // Vengono aggiunte con setSection('danger') e poi spostate prima di "Elimina file"
+ // tramite (menu as any).items — l'unico modo per posizionarle nell'ultima sezione
+ // sopra Delete senza usare API private più instabili.
+ this.registerEvent(
+ this.app.workspace.on('file-menu', (menu, file) => {
+ if (!(file instanceof TFile) || file.extension !== 'md') return;
+ menu.addItem(item => item
+ .setTitle('Espandi tutti i disegni')
+ .setIcon('chevrons-down')
+ .setSection('danger')
+ .onClick(() => {
+ this.getActiveEmbeds(file.path).forEach(a => a.expand());
+ })
+ );
+ menu.addItem(item => item
+ .setTitle('Collassa tutti i disegni')
+ .setIcon('chevrons-up')
+ .setSection('danger')
+ .onClick(() => {
+ this.getActiveEmbeds(file.path).forEach(a => a.collapse());
+ })
+ );
+ menu.addItem(item => item
+ .setTitle('Converti tutti i disegni in testo')
+ .setIcon('file-text')
+ .setSection('danger')
+ .onClick(async () => {
+ try {
+ // Sequenziale: si ferma al primo errore
+ for (const actions of this.getActiveEmbeds(file.path)) {
+ await actions.convert();
+ }
+ } catch (e: unknown) {
+ new Notice('Errore nella conversione: ' + (e instanceof Error ? e.message : String(e)));
+ }
+ })
+ );
+ // Sposta le 3 voci appena aggiunte prima del primo item 'danger' esistente
+ // (cioè prima di "Elimina file"), in modo che compaiano sopra di esso.
+ const items: Array<{ section: string }> = (menu as any).items;
+ const added = items.splice(items.length - 3, 3);
+ const firstDangerIdx = items.findIndex(i => i.section === 'danger');
+ items.splice(firstDangerIdx >= 0 ? firstDangerIdx : items.length, 0, ...added);
+ })
+ );
+ }
+
+ // Restituisce gli embed attivi (container nel DOM) appartenenti al file indicato.
+ // Rimuove dalla mappa gli embed il cui container non è più nel DOM.
+ private getActiveEmbeds(filePath: string) {
+ const result: Array<{ expand: () => void; collapse: () => void; convert: () => Promise }> = [];
+ for (const [id, actions] of this.embedActions) {
+ if (!actions.container.isConnected) {
+ this.embedActions.delete(id);
+ continue;
+ }
+ if (actions.sourcePath === filePath) result.push(actions);
+ }
+ return result;
}
async loadSettings() {