2026-03-15 12:25:50 +00:00
|
|
|
/* =============================================
|
|
|
|
|
Handwriting to Markdown — Plugin Entry Point
|
|
|
|
|
|
|
|
|
|
Registra:
|
|
|
|
|
- Code block processor "handwriting" (embed inline)
|
|
|
|
|
- Comando per inserire un nuovo blocco handwriting
|
|
|
|
|
- Tab impostazioni
|
|
|
|
|
============================================= */
|
|
|
|
|
|
2026-03-24 17:28:50 +00:00
|
|
|
import { Plugin, TFile, TFolder, Notice, FuzzySuggestModal, FuzzyMatch, MarkdownView, Editor } from 'obsidian';
|
|
|
|
|
import { t, setLocale } from './i18n';
|
2026-03-15 12:25:50 +00:00
|
|
|
import { DEFAULT_SETTINGS, HandwritingSettings, HandwritingSettingTab } from './settings';
|
|
|
|
|
import { registerEmbed, insertHandwritingBlock } from './embed';
|
2026-03-15 23:03:31 +00:00
|
|
|
import { VIEW_TYPE_HANDWRITING, DrawingEditorView } from './editor-view';
|
2026-03-15 12:25:50 +00:00
|
|
|
|
|
|
|
|
export default class HandwritingPlugin extends Plugin {
|
|
|
|
|
settings: HandwritingSettings;
|
|
|
|
|
|
2026-03-15 23:03:31 +00:00
|
|
|
// Mappa di callback per aggiornare le preview inline quando l'editor tab salva
|
|
|
|
|
public previewCallbacks = new Map<string, (svgContent: string) => void>();
|
|
|
|
|
|
2026-03-23 01:03:11 +00:00
|
|
|
// Mappa embedId → svgPath: permette di trovare i file SVG da rimappare al cambio bgMode
|
|
|
|
|
public embedPaths = new Map<string, string>();
|
|
|
|
|
|
|
|
|
|
// Callback notificate quando l'utente cambia bgMode nelle impostazioni
|
|
|
|
|
public bgModeListeners = new Set<(bgMode: string) => void>();
|
|
|
|
|
|
2026-03-23 17:41:59 +00:00
|
|
|
// Mappa embedId → azioni (expand/collapse/convert): usata dal menu "⋮" di Obsidian
|
|
|
|
|
public embedActions = new Map<string, {
|
|
|
|
|
expand: () => void;
|
|
|
|
|
collapse: () => void;
|
|
|
|
|
convert: () => Promise<void>;
|
|
|
|
|
container: HTMLElement;
|
|
|
|
|
sourcePath: string;
|
|
|
|
|
}>();
|
|
|
|
|
|
2026-03-15 23:03:31 +00:00
|
|
|
// Invocato dall'editor tab dopo ogni salvataggio per aggiornare la preview inline
|
|
|
|
|
refreshPreview(id: string, svgContent: string) {
|
|
|
|
|
this.previewCallbacks.get(id)?.(svgContent);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 01:03:11 +00:00
|
|
|
// Chiamato da settings quando l'utente cambia bgMode:
|
|
|
|
|
// notifica pannelli (aggiornamento classe dark) e SVG attivi (remap colori)
|
|
|
|
|
notifyBgModeChange() {
|
|
|
|
|
this.bgModeListeners.forEach(cb => cb(this.settings.bgMode));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 12:25:50 +00:00
|
|
|
async onload() {
|
|
|
|
|
await this.loadSettings();
|
|
|
|
|
|
2026-03-24 17:28:50 +00:00
|
|
|
// Applica la lingua interfaccia salvata (o la lingua di sistema se 'auto')
|
|
|
|
|
setLocale(this.settings.uiLanguage);
|
|
|
|
|
|
2026-03-23 14:15:12 +00:00
|
|
|
// Rileva cambio tema Obsidian (aggiunta/rimozione classe 'theme-dark' sul body).
|
|
|
|
|
// Se bgMode è 'auto', notifica tutti i listener per aggiornare colori e SVG.
|
|
|
|
|
const themeObserver = new MutationObserver(() => {
|
|
|
|
|
if (this.settings.bgMode === 'auto') this.notifyBgModeChange();
|
|
|
|
|
});
|
|
|
|
|
themeObserver.observe(document.body, { attributeFilter: ['class'] });
|
|
|
|
|
this.register(() => themeObserver.disconnect());
|
|
|
|
|
|
2026-03-15 23:03:31 +00:00
|
|
|
// Registra la vista editor (tab dedicata per il disegno)
|
|
|
|
|
this.registerView(VIEW_TYPE_HANDWRITING, (leaf) => new DrawingEditorView(leaf, this));
|
|
|
|
|
|
2026-03-15 12:25:50 +00:00
|
|
|
// Registra il code block processor per ```handwriting
|
|
|
|
|
registerEmbed(this);
|
|
|
|
|
|
|
|
|
|
// Comando: inserisce un nuovo blocco handwriting nel file corrente
|
|
|
|
|
this.addCommand({
|
|
|
|
|
id: 'insert-handwriting',
|
|
|
|
|
name: 'Insert handwriting block',
|
2026-03-24 17:28:50 +00:00
|
|
|
icon: 'pencil',
|
2026-03-15 23:03:31 +00:00
|
|
|
editorCallback: async () => {
|
|
|
|
|
await insertHandwritingBlock(this);
|
2026-03-15 12:25:50 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-24 17:28:50 +00:00
|
|
|
// Comando: inserisce un riferimento a un SVG esistente nella cartella handwriting
|
|
|
|
|
this.addCommand({
|
|
|
|
|
id: 'insert-svg-reference',
|
|
|
|
|
name: 'Insert SVG reference',
|
|
|
|
|
icon: 'file-plus',
|
|
|
|
|
editorCallback: (editor: Editor) => {
|
|
|
|
|
new SvgReferenceSuggest(this.app, this, editor).open();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-15 12:25:50 +00:00
|
|
|
// Icona nella ribbon (sidebar sinistra)
|
2026-03-15 23:03:31 +00:00
|
|
|
this.addRibbonIcon('pencil', 'Insert handwriting', async () => {
|
|
|
|
|
await insertHandwritingBlock(this);
|
2026-03-15 12:25:50 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Tab impostazioni
|
|
|
|
|
this.addSettingTab(new HandwritingSettingTab(this.app, this));
|
2026-03-23 17:41:59 +00:00
|
|
|
|
|
|
|
|
// 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
|
2026-03-24 17:28:50 +00:00
|
|
|
.setTitle(t('menu_expand_all'))
|
2026-03-23 17:41:59 +00:00
|
|
|
.setIcon('chevrons-down')
|
|
|
|
|
.setSection('danger')
|
|
|
|
|
.onClick(() => {
|
|
|
|
|
this.getActiveEmbeds(file.path).forEach(a => a.expand());
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
menu.addItem(item => item
|
2026-03-24 17:28:50 +00:00
|
|
|
.setTitle(t('menu_collapse_all'))
|
2026-03-23 17:41:59 +00:00
|
|
|
.setIcon('chevrons-up')
|
|
|
|
|
.setSection('danger')
|
|
|
|
|
.onClick(() => {
|
|
|
|
|
this.getActiveEmbeds(file.path).forEach(a => a.collapse());
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
menu.addItem(item => item
|
2026-03-24 17:28:50 +00:00
|
|
|
.setTitle(t('menu_convert_all'))
|
2026-03-23 17:41:59 +00:00
|
|
|
.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) {
|
2026-03-24 17:28:50 +00:00
|
|
|
new Notice(t('error_conversion') + (e instanceof Error ? e.message : String(e)));
|
2026-03-23 17:41:59 +00:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
// 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<void> }> = [];
|
|
|
|
|
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;
|
2026-03-15 12:25:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async loadSettings() {
|
|
|
|
|
this.settings = Object.assign(
|
|
|
|
|
{},
|
|
|
|
|
DEFAULT_SETTINGS,
|
|
|
|
|
await this.loadData() as Partial<HandwritingSettings>
|
|
|
|
|
);
|
2026-03-23 14:15:12 +00:00
|
|
|
// Migrazione: 'custom' non esiste più → 'auto'
|
|
|
|
|
if ((this.settings.bgMode as string) === 'custom') {
|
|
|
|
|
this.settings.bgMode = 'auto';
|
|
|
|
|
}
|
2026-03-15 12:25:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async saveSettings() {
|
|
|
|
|
await this.saveData(this.settings);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-24 17:28:50 +00:00
|
|
|
|
|
|
|
|
// Modal fuzzy-search per selezionare un SVG esistente nella cartella handwriting
|
|
|
|
|
// e inserire il riferimento ![[path]] nel cursore dell'editor attivo.
|
|
|
|
|
class SvgReferenceSuggest extends FuzzySuggestModal<TFile> {
|
|
|
|
|
constructor(
|
|
|
|
|
app: import('obsidian').App,
|
|
|
|
|
private plugin: HandwritingPlugin,
|
|
|
|
|
private editor: Editor
|
|
|
|
|
) {
|
|
|
|
|
super(app);
|
|
|
|
|
this.setPlaceholder('Cerca SVG...');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Restituisce tutti gli SVG nella cartella impostata (esclusa _converted)
|
|
|
|
|
getItems(): TFile[] {
|
|
|
|
|
const folder = this.app.vault.getAbstractFileByPath(this.plugin.settings.svgFolder);
|
|
|
|
|
if (!(folder instanceof TFolder)) return [];
|
|
|
|
|
return folder.children.filter(
|
|
|
|
|
(f): f is TFile =>
|
|
|
|
|
f instanceof TFile &&
|
|
|
|
|
f.extension === 'svg' &&
|
|
|
|
|
!f.path.includes('/_converted/')
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Testo usato per il fuzzy-match (nome file)
|
|
|
|
|
getItemText(file: TFile): string {
|
|
|
|
|
return file.name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mostra thumbnail SVG + nome file invece del solo testo
|
|
|
|
|
renderSuggestion(match: FuzzyMatch<TFile>, el: HTMLElement): void {
|
|
|
|
|
const file = match.item;
|
|
|
|
|
el.addClass('hwm_svg-suggest-item');
|
|
|
|
|
// Thumbnail SVG tramite resource URL del vault
|
|
|
|
|
const img = el.createEl('img', { cls: 'hwm_svg-thumb' });
|
|
|
|
|
img.src = this.app.vault.getResourcePath(file);
|
|
|
|
|
el.createEl('span', { text: file.name, cls: 'hwm_svg-suggest-name' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Inserisce ![[path]] al cursore quando l'utente seleziona un file
|
|
|
|
|
onChooseItem(file: TFile): void {
|
|
|
|
|
this.editor.replaceSelection(`![[${file.path}]]`);
|
|
|
|
|
}
|
|
|
|
|
}
|