chore: initial project commit

This commit is contained in:
Ignacio Ballesteros
2026-02-17 21:25:46 +01:00
commit 70f7fac23e
34 changed files with 6786 additions and 0 deletions

53
src/addon.ts Normal file
View File

@@ -0,0 +1,53 @@
import { OrgExporter } from "./modules/exporter";
import { Prefs } from "./modules/prefs";
export interface AddonInfo {
id: string;
version: string;
rootURI: string;
}
export class OrgExportAnnotationsAddon {
public id!: string;
public version!: string;
public rootURI!: string;
public data = {
env: typeof __env__ !== "undefined" ? __env__ : "production",
initialized: false,
};
public prefs!: Prefs;
public exporter!: OrgExporter;
init(info: AddonInfo): void {
this.id = info.id;
this.version = info.version;
this.rootURI = info.rootURI;
this.prefs = new Prefs();
this.exporter = new OrgExporter(this.prefs);
this.log(`Initialized version ${this.version}`);
}
log(message: string, ...args: unknown[]): void {
if (this.data.env === "development" || this.prefs?.debug) {
Zotero.debug(`[OrgExportAnnotations] ${message}`, false);
if (args.length > 0) {
Zotero.debug(JSON.stringify(args, null, 2), false);
}
}
}
error(message: string, error?: Error): void {
Zotero.logError(`[OrgExportAnnotations] ${message}`);
if (error) {
Zotero.logError(error);
}
}
warn(message: string): void {
Zotero.debug(`[OrgExportAnnotations] [WARN] ${message}`, false);
}
}

157
src/hooks.ts Normal file
View File

@@ -0,0 +1,157 @@
import type { OrgExportAnnotationsAddon } from "./addon";
import { registerNotifiers, unregisterNotifiers } from "./modules/notifier";
import { registerMenus, unregisterMenus } from "./modules/menu";
export interface Hooks {
onStartup(): Promise<void>;
onShutdown(): void;
onMainWindowLoad(window: Window): Promise<void>;
onMainWindowUnload(window: Window): Promise<void>;
onMenuExportAll(): Promise<void>;
onMenuForceExportAll(): Promise<void>;
onMenuExportSelected(): Promise<void>;
}
function showNotification(message: string): void {
const pw = new Zotero.ProgressWindow({ closeOnClick: true });
pw.changeHeadline("Org Export");
pw.addDescription(message);
pw.show();
pw.startCloseTimer(4000);
}
export function createHooks(addon: OrgExportAnnotationsAddon): Hooks {
let notifierIDs: string[] = [];
return {
async onStartup(): Promise<void> {
addon.log("Starting up...");
try {
// Wait for Zotero UI to be fully ready before registering menus
await Promise.all([
Zotero.initializationPromise,
Zotero.unlockPromise,
Zotero.uiReadyPromise,
]);
if (!Zotero.BetterNotes) {
addon.warn("Better Notes plugin not found - some features may not work");
}
Zotero.PreferencePanes.register({
pluginID: addon.id,
src: addon.rootURI + "content/preferences.xhtml",
scripts: [addon.rootURI + "content/scripts/preferences.js"],
label: "Org Export",
image: addon.rootURI + "content/icons/icon.svg",
});
notifierIDs = registerNotifiers(addon);
// Register menus for any windows that are already open
// (onMainWindowLoad may have fired before our startup completed)
const mainWindows = Zotero.getMainWindows();
for (const win of mainWindows) {
await this.onMainWindowLoad(win);
}
addon.data.initialized = true;
addon.log("Startup complete");
} catch (error) {
addon.error("Startup failed", error as Error);
addon.data.initialized = false;
throw error;
}
},
onShutdown(): void {
addon.log("Shutting down...");
try {
unregisterNotifiers(notifierIDs);
notifierIDs = [];
addon.data.initialized = false;
addon.log("Shutdown complete");
} catch (error) {
addon.error("Shutdown failed", error as Error);
}
},
async onMainWindowLoad(window: Window): Promise<void> {
addon.log("Main window loaded");
registerMenus(addon, window);
},
async onMainWindowUnload(window: Window): Promise<void> {
addon.log("Main window unloading");
unregisterMenus(window);
},
async onMenuExportAll(): Promise<void> {
addon.log("Menu: Export All triggered");
try {
const count = await addon.exporter.syncAll();
if (addon.prefs.showNotification) {
const message =
count > 0
? `Exported ${count} item${count === 1 ? "" : "s"} to Org`
: "No items needed export";
showNotification(message);
}
} catch (error) {
addon.error("Export All failed", error as Error);
showNotification(`Export failed: ${(error as Error).message}`);
}
},
async onMenuForceExportAll(): Promise<void> {
addon.log("Menu: Force Export All triggered");
try {
const count = await addon.exporter.forceExportAll();
if (addon.prefs.showNotification) {
const message =
count > 0
? `Force exported ${count} item${count === 1 ? "" : "s"} to Org`
: "No items with annotations to export";
showNotification(message);
}
} catch (error) {
addon.error("Force Export All failed", error as Error);
showNotification(`Export failed: ${(error as Error).message}`);
}
},
async onMenuExportSelected(): Promise<void> {
addon.log("Menu: Export Selected triggered");
try {
const zoteroPane = Zotero.getActiveZoteroPane();
const selectedItems = zoteroPane.getSelectedItems();
if (selectedItems.length === 0) {
showNotification("No items selected");
return;
}
let count = 0;
for (const item of selectedItems) {
const exported = await addon.exporter.exportItem(item);
if (exported) count++;
}
if (addon.prefs.showNotification) {
const message =
count > 0
? `Exported ${count} item${count === 1 ? "" : "s"} to Org`
: "No annotations found in selected items";
showNotification(message);
}
} catch (error) {
addon.error("Export Selected failed", error as Error);
showNotification(`Export failed: ${(error as Error).message}`);
}
},
};
}

31
src/index.ts Normal file
View File

@@ -0,0 +1,31 @@
import { OrgExportAnnotationsAddon } from "./addon";
import { createHooks } from "./hooks";
const addon = new OrgExportAnnotationsAddon();
const hooks = createHooks(addon);
Zotero.OrgExportAnnotations = {
init: addon.init.bind(addon),
log: addon.log.bind(addon),
error: addon.error.bind(addon),
warn: addon.warn.bind(addon),
get id() {
return addon.id;
},
get version() {
return addon.version;
},
get rootURI() {
return addon.rootURI;
},
get data() {
return addon.data;
},
get prefs() {
return addon.prefs;
},
get exporter() {
return addon.exporter;
},
hooks,
};

58
src/modules/converter.ts Normal file
View File

@@ -0,0 +1,58 @@
export async function convertToOrg(
note: Zotero.Item,
notesPath: string,
orgPath: string,
title: string,
pandocPath: string
): Promise<void> {
const citationKey = note.parentItem?.getField("citationKey") || "temp";
const mdPath = PathUtils.join(notesPath, `${citationKey}.md`);
try {
const mdContent = await Zotero.BetterNotes!.api.convert.note2md(note, notesPath);
await Zotero.File.putContentsAsync(mdPath, mdContent);
await Zotero.Utilities.Internal.exec(pandocPath, [
"--from=markdown",
"--to=org",
`--output=${orgPath}`,
mdPath,
]);
await prependOrgTitle(orgPath, title);
await cleanupTempFile(mdPath);
} catch (error) {
try {
await cleanupTempFile(mdPath);
} catch {
// Ignore cleanup errors
}
throw error;
}
}
async function prependOrgTitle(orgPath: string, title: string): Promise<void> {
const orgContent = await Zotero.File.getContentsAsync(orgPath);
const withTitle = `#+title: ${title}\n${orgContent}`;
await Zotero.File.putContentsAsync(orgPath, withTitle);
}
async function cleanupTempFile(path: string): Promise<void> {
try {
if (await IOUtils.exists(path)) {
await IOUtils.remove(path);
}
} catch (_error) {
Zotero.OrgExportAnnotations.warn(`Failed to clean up temp file: ${path}`);
}
}
export async function testPandoc(pandocPath: string): Promise<boolean> {
try {
await Zotero.Utilities.Internal.exec(pandocPath, ["--version"]);
return true;
} catch (_error) {
return false;
}
}

280
src/modules/exporter.ts Normal file
View File

@@ -0,0 +1,280 @@
import type { Prefs } from "./prefs";
import { convertToOrg } from "./converter";
export class OrgExporter {
constructor(private prefs: Prefs) {}
isAttachmentWithExtractableAnnotations(item: Zotero.Item): boolean {
if (!item.isFileAttachment()) {
Zotero.OrgExportAnnotations.warn(`Item ${item.id}: not a file attachment`);
return false;
}
const isSupported =
item.isPDFAttachment() || item.isEPUBAttachment() || item.isSnapshotAttachment();
if (!isSupported) {
Zotero.OrgExportAnnotations.warn(
`Item ${item.id}: unsupported attachment type (not PDF/EPUB/Snapshot)`
);
return false;
}
try {
const annotations = item.getAnnotations();
Zotero.OrgExportAnnotations.warn(
`Item ${item.id}: found ${annotations?.length ?? 0} annotations`
);
return annotations && annotations.length > 0;
} catch (error) {
Zotero.OrgExportAnnotations.error("Failed to check annotations", error as Error);
return false;
}
}
private isAnnotationNote(noteHTML: string): boolean {
return (
noteHTML.startsWith("<div data-citation-items") ||
noteHTML.startsWith("<div datap-citation-items") ||
noteHTML.startsWith("<h1>Annotations")
);
}
private async getLastAnnotationsNote(item: Zotero.Item): Promise<Zotero.Item | undefined> {
const noteIDs = item.getNotes();
const notes = Zotero.Items.get(noteIDs).filter((note) => this.isAnnotationNote(note.getNote()));
if (notes.length === 0) return undefined;
return notes.reduce((max, current) =>
current.dateModified > max.dateModified ? current : max
);
}
private async removeExistingNotes(item: Zotero.Item): Promise<void> {
try {
const noteIDs = item.getNotes();
const notes = Zotero.Items.get(noteIDs).filter((note) =>
this.isAnnotationNote(note.getNote())
);
for (const note of notes) {
await Zotero.Items.trashTx(note.id);
Zotero.OrgExportAnnotations.log(`Removed old annotation note: ${note.id}`);
}
} catch (error) {
Zotero.OrgExportAnnotations.error("Failed to remove existing notes", error as Error);
}
}
private getAttachmentLastModifiedAt(attachment: Zotero.Item): string | undefined {
if (!this.isAttachmentWithExtractableAnnotations(attachment)) {
return undefined;
}
const annotations = attachment.getAnnotations();
if (!annotations || annotations.length === 0) return undefined;
return annotations.reduce((max, current) =>
current.dateModified > max.dateModified ? current : max
).dateModified;
}
async needsOrgSync(attachment: Zotero.Item): Promise<boolean> {
if (!attachment.parentItem) return false;
const note = await this.getLastAnnotationsNote(attachment.parentItem);
const annotationDateModified = this.getAttachmentLastModifiedAt(attachment);
if (note && annotationDateModified) {
return annotationDateModified > note.dateModified;
}
if (annotationDateModified && !note) {
return true;
}
return false;
}
private async createNote(attachment: Zotero.Item): Promise<Zotero.Item | undefined> {
try {
const ZoteroPane = Zotero.getActiveZoteroPane();
const note = await ZoteroPane.addNoteFromAnnotationsForAttachment(attachment, {
skipSelect: true,
});
Zotero.OrgExportAnnotations.log(`Created note from annotations: ${note?.id}`);
return note;
} catch (error) {
Zotero.OrgExportAnnotations.error("Failed to create note", error as Error);
throw error;
}
}
private existsOrgAttachment(parentItem: Zotero.Item, path: string): boolean {
try {
const attachmentIDs = parentItem.getAttachments();
const attachments = Zotero.Items.get(attachmentIDs);
return attachments.some((a) => a.attachmentPath === path);
} catch (error) {
Zotero.OrgExportAnnotations.error(
"Failed to check for existing org attachment",
error as Error
);
return false;
}
}
async exportAttachment(attachment: Zotero.Item): Promise<boolean> {
Zotero.OrgExportAnnotations.warn(`exportAttachment called: id=${attachment.id}`);
if (!this.isAttachmentWithExtractableAnnotations(attachment)) {
return false;
}
if (!attachment.parentItem) {
Zotero.OrgExportAnnotations.warn("Attachment has no parent item");
return false;
}
const notesPath = this.prefs.notesPath;
Zotero.OrgExportAnnotations.warn(`notesPath = "${notesPath}"`);
if (!notesPath) {
throw new Error("Notes path is not configured");
}
if (!Zotero.BetterNotes?.api?.convert?.note2md) {
throw new Error("Better Notes plugin is required but not found");
}
await this.prefs.ensureNotesDirectory();
await this.removeExistingNotes(attachment.parentItem);
const note = await this.createNote(attachment);
if (!note) {
Zotero.OrgExportAnnotations.warn("Failed to create note for attachment");
return false;
}
const title = attachment.parentItem.getField("title") as string;
const citationKey = attachment.parentItem.getField("citationKey") as string;
Zotero.OrgExportAnnotations.warn(`title="${title}", citationKey="${citationKey}"`);
if (!citationKey) {
Zotero.OrgExportAnnotations.warn(`Item "${title}" has no citation key, skipping`);
return false;
}
const orgPath = PathUtils.join(notesPath, `${citationKey}.org`);
Zotero.OrgExportAnnotations.warn(`Converting to org at: ${orgPath}`);
await convertToOrg(note, notesPath, orgPath, title, this.prefs.pandocPath);
if (this.prefs.attachOrgFile && !this.existsOrgAttachment(attachment.parentItem, orgPath)) {
await Zotero.Attachments.linkFromFile({
file: orgPath,
parentItemID: attachment.parentItem.id,
contentType: "text/org",
title: `${title} (Org)`,
});
}
Zotero.OrgExportAnnotations.warn(`Exported to ${orgPath}`);
return true;
}
async exportItem(item: Zotero.Item): Promise<boolean> {
Zotero.OrgExportAnnotations.warn(
`exportItem called: id=${item.id}, isAttachment=${item.isAttachment()}, isRegularItem=${item.isRegularItem()}`
);
if (item.isAttachment()) {
return this.exportAttachment(item);
}
if (!item.isRegularItem()) {
Zotero.OrgExportAnnotations.warn(`Item ${item.id}: not a regular item, skipping`);
return false;
}
const attachmentIDs = item.getAttachments();
const attachments = Zotero.Items.get(attachmentIDs);
Zotero.OrgExportAnnotations.warn(
`Item ${item.id}: found ${attachments.length} attachments (IDs: ${attachmentIDs.join(", ")})`
);
let exported = false;
for (const attachment of attachments) {
if (await this.exportAttachment(attachment)) {
exported = true;
}
}
return exported;
}
async listAllAttachments(): Promise<Zotero.Item[]> {
try {
const search = new Zotero.Search();
search.libraryID = Zotero.Libraries.userLibraryID;
search.addCondition("itemType", "is", "attachment");
const itemIDs = await search.search();
const uniqueIDs = [...new Set(itemIDs)];
const items = Zotero.Items.get(uniqueIDs);
return items
.filter((item) => this.isAttachmentWithExtractableAnnotations(item))
.filter((item) => item.parentItem !== undefined);
} catch (error) {
Zotero.OrgExportAnnotations.error("Failed to list attachments", error as Error);
return [];
}
}
async syncAll(): Promise<number> {
const attachments = await this.listAllAttachments();
let count = 0;
for (const attachment of attachments) {
const needsSync = await this.needsOrgSync(attachment);
if (needsSync) {
try {
if (await this.exportAttachment(attachment)) {
count++;
}
} catch (error) {
Zotero.OrgExportAnnotations.error(
`Failed to export attachment ${attachment.id}`,
error as Error
);
}
}
}
Zotero.OrgExportAnnotations.log(`Synced ${count} attachments`);
return count;
}
async forceExportAll(): Promise<number> {
const attachments = await this.listAllAttachments();
let count = 0;
for (const attachment of attachments) {
try {
if (await this.exportAttachment(attachment)) {
count++;
}
} catch (error) {
Zotero.OrgExportAnnotations.error(
`Failed to force export attachment ${attachment.id}`,
error as Error
);
}
}
Zotero.OrgExportAnnotations.log(`Force exported ${count} attachments`);
return count;
}
}

159
src/modules/menu.ts Normal file
View File

@@ -0,0 +1,159 @@
import type { OrgExportAnnotationsAddon } from "../addon";
const MENU_IDS = {
toolsExportAll: "orgexportannotations-menu-tools-export-all",
itemExportSelected: "orgexportannotations-menu-item-export",
itemSeparator: "orgexportannotations-menu-separator",
};
// Track registered menu IDs for Zotero 8 MenuManager API
let registeredMenuIDs: string[] = [];
export function registerMenus(addon: OrgExportAnnotationsAddon, window: Window): void {
// Use new MenuManager API if available (Zotero 8+)
if (Zotero.MenuManager) {
registerMenusZotero8(addon);
return;
}
// Fall back to DOM manipulation for Zotero 7
registerMenusZotero7(addon, window);
}
function registerMenusZotero8(addon: OrgExportAnnotationsAddon): void {
// Guard against double registration
if (registeredMenuIDs.length > 0) {
addon.log("MenuManager menus already registered, skipping");
return;
}
try {
// Register item context menu
Zotero.MenuManager!.registerMenu({
menuID: MENU_IDS.itemExportSelected,
pluginID: addon.id,
target: "main/library/item",
menus: [
{
menuType: "menuitem",
icon: addon.rootURI + "content/icons/menu-icon.svg",
onShowing: (_event, context) => {
context.menuElem?.setAttribute("label", "Export Annotations to Org");
},
onCommand: () => Zotero.OrgExportAnnotations.hooks.onMenuExportSelected(),
},
],
});
registeredMenuIDs.push(MENU_IDS.itemExportSelected);
addon.log("Registered item context menu via MenuManager");
// Register Tools menu item
Zotero.MenuManager!.registerMenu({
menuID: MENU_IDS.toolsExportAll,
pluginID: addon.id,
target: "main/menubar/tools",
menus: [
{
menuType: "menuitem",
icon: addon.rootURI + "content/icons/menu-icon.svg",
onShowing: (_event, context) => {
context.menuElem?.setAttribute("label", "Export All Annotations to Org");
},
onCommand: () => Zotero.OrgExportAnnotations.hooks.onMenuExportAll(),
},
],
});
registeredMenuIDs.push(MENU_IDS.toolsExportAll);
addon.log("Registered Tools menu item via MenuManager");
} catch (error) {
addon.error("Failed to register menus via MenuManager", error as Error);
}
}
function registerMenusZotero7(addon: OrgExportAnnotationsAddon, window: Window): void {
const doc = window.document;
// Guard against double registration (onStartup and onMainWindowLoad may both fire)
if (doc.getElementById(MENU_IDS.itemExportSelected)) {
addon.log("Menu items already registered, skipping");
return;
}
// Item context menu — DOM append to zotero-itemmenu
const itemMenu = doc.getElementById("zotero-itemmenu");
if (itemMenu) {
const separator = doc.createXULElement("menuseparator");
separator.id = MENU_IDS.itemSeparator;
itemMenu.appendChild(separator);
const menuItem = createMenuItem(doc, {
id: MENU_IDS.itemExportSelected,
label: "Export Annotations to Org",
icon: addon.rootURI + "content/icons/menu-icon.svg",
oncommand: () => Zotero.OrgExportAnnotations.hooks.onMenuExportSelected(),
});
itemMenu.appendChild(menuItem);
addon.log("Added item context menu item");
} else {
addon.warn("Could not find zotero-itemmenu element");
}
// Tools menu — direct DOM append
const toolsMenu = doc.getElementById("menu_ToolsPopup");
if (toolsMenu) {
const menuItem = createMenuItem(doc, {
id: MENU_IDS.toolsExportAll,
label: "Export All Annotations to Org",
icon: addon.rootURI + "content/icons/menu-icon.svg",
oncommand: () => Zotero.OrgExportAnnotations.hooks.onMenuExportAll(),
});
toolsMenu.appendChild(menuItem);
addon.log("Added Tools menu item");
} else {
addon.warn("Could not find menu_ToolsPopup element");
}
}
export function unregisterMenus(window: Window): void {
// For Zotero 8+, MenuManager handles cleanup automatically when plugin is disabled
// But we can manually unregister if needed
if (Zotero.MenuManager && registeredMenuIDs.length > 0) {
for (const menuID of registeredMenuIDs) {
try {
Zotero.MenuManager.unregisterMenu(menuID);
} catch (_error) {
// Menu may already be unregistered
}
}
registeredMenuIDs = [];
return;
}
// For Zotero 7, remove DOM elements
const doc = window.document;
const idsToRemove = Object.values(MENU_IDS);
for (const id of idsToRemove) {
const element = doc.getElementById(id);
element?.remove();
}
}
function createMenuItem(
doc: Document,
options: {
id: string;
label: string;
icon?: string;
oncommand: () => void;
}
): Element {
const menuItem = doc.createXULElement("menuitem");
menuItem.id = options.id;
menuItem.setAttribute("label", options.label);
if (options.icon) {
menuItem.setAttribute("class", "menuitem-iconic");
menuItem.setAttribute("image", options.icon);
}
menuItem.addEventListener("command", options.oncommand);
return menuItem;
}

73
src/modules/notifier.ts Normal file
View File

@@ -0,0 +1,73 @@
import type { OrgExportAnnotationsAddon } from "../addon";
export function registerNotifiers(addon: OrgExportAnnotationsAddon): string[] {
const ids: string[] = [];
const syncNotifierID = Zotero.Notifier.registerObserver(
{
notify: async (
event: string,
type: string,
_ids: number[],
_extraData: Record<string, unknown>
) => {
if (event === "finish" && type === "sync") {
addon.log("Sync completed, checking for exports...");
if (addon.prefs.autoExportOnSync) {
try {
await addon.exporter.syncAll();
} catch (error) {
addon.error("Auto-export after sync failed", error as Error);
}
}
}
},
},
["sync"],
"orgexportannotations-sync"
);
ids.push(syncNotifierID);
const tabNotifierID = Zotero.Notifier.registerObserver(
{
notify: async (
event: string,
type: string,
ids: number[],
extraData: Record<string, unknown>
) => {
if (event === "close" && type === "tab") {
if (!addon.prefs.exportOnTabClose) return;
addon.log("Tab closed, checking for export...");
for (const tabId of ids) {
try {
const tabInfo = extraData[tabId] as { itemID?: number } | undefined;
if (tabInfo?.itemID) {
const item = Zotero.Items.get(tabInfo.itemID);
if (item) {
await addon.exporter.exportItem(item);
}
}
} catch (error) {
addon.error(`Failed to export on tab close: ${tabId}`, error as Error);
}
}
}
},
},
["tab"],
"orgexportannotations-tab"
);
ids.push(tabNotifierID);
addon.log(`Registered ${ids.length} notifiers`);
return ids;
}
export function unregisterNotifiers(ids: string[]): void {
for (const id of ids) {
Zotero.Notifier.unregisterObserver(id);
}
}

103
src/modules/prefs.ts Normal file
View File

@@ -0,0 +1,103 @@
const PREF_PREFIX = "extensions.zotero.orgexportannotations";
export class Prefs {
private getPref<T>(key: string, defaultValue: T): T {
const fullKey = `${PREF_PREFIX}.${key}`;
const value = Zotero.Prefs.get(fullKey, true);
return (value as T) ?? defaultValue;
}
private setPref<T>(key: string, value: T): void {
const fullKey = `${PREF_PREFIX}.${key}`;
Zotero.Prefs.set(fullKey, value, true);
}
get notesPath(): string {
const path = this.getPref("notesPath", "");
if (path && path.startsWith("~")) {
try {
// Use Gecko's directory service to get the actual home directory
const homeDir = Services.dirsvc.get("Home", Ci.nsIFile).path;
return path.replace("~", homeDir);
} catch (_error) {
// Fallback: strip the Zotero profile-specific suffix from profileDir
const home = PathUtils.profileDir.replace(/[/\\][^/\\]+[/\\][^/\\]+$/, "");
return path.replace("~", home);
}
}
return path;
}
set notesPath(value: string) {
this.setPref("notesPath", value);
}
get pandocPath(): string {
return this.getPref("pandocPath", "pandoc");
}
set pandocPath(value: string) {
this.setPref("pandocPath", value);
}
get attachOrgFile(): boolean {
return this.getPref("attachOrgFile", false);
}
set attachOrgFile(value: boolean) {
this.setPref("attachOrgFile", value);
}
get autoExportOnSync(): boolean {
return this.getPref("autoExportOnSync", true);
}
set autoExportOnSync(value: boolean) {
this.setPref("autoExportOnSync", value);
}
get exportOnTabClose(): boolean {
return this.getPref("exportOnTabClose", true);
}
set exportOnTabClose(value: boolean) {
this.setPref("exportOnTabClose", value);
}
get showNotification(): boolean {
return this.getPref("showNotification", true);
}
set showNotification(value: boolean) {
this.setPref("showNotification", value);
}
get debug(): boolean {
return this.getPref("debug", false);
}
set debug(value: boolean) {
this.setPref("debug", value);
}
async isNotesPathValid(): Promise<boolean> {
const path = this.notesPath;
if (!path) return false;
try {
return await IOUtils.exists(path);
} catch {
return false;
}
}
async ensureNotesDirectory(): Promise<void> {
const path = this.notesPath;
if (!path) {
throw new Error("Notes path is not configured");
}
await IOUtils.makeDirectory(path, { ignoreExisting: true });
const attachmentsPath = PathUtils.join(path, "attachments");
await IOUtils.makeDirectory(attachmentsPath, { ignoreExisting: true });
}
}