diff --git a/package.json b/package.json index dd563fc..59017f3 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "yamljs": "^0.3.0", - "zotero-plugin-toolkit": "^2.2.5" + "zotero-plugin-toolkit": "^2.3.2" }, "devDependencies": { "@esbuild-plugins/node-globals-polyfill": "^0.2.3", diff --git a/src/addon.ts b/src/addon.ts index be2a051..3ad1f04 100644 --- a/src/addon.ts +++ b/src/addon.ts @@ -3,6 +3,7 @@ import { ColumnOptions, VirtualizedTableHelper, } from "zotero-plugin-toolkit/dist/helpers/virtualizedTable"; +import { LargePrefHelper } from "zotero-plugin-toolkit/dist/helpers/largePref"; import ToolkitGlobal from "zotero-plugin-toolkit/dist/managers/toolkitGlobal"; import { getPref, setPref } from "./utils/prefs"; @@ -30,6 +31,7 @@ class Addon { pdf: { promise?: _ZoteroTypes.PromiseObject }; }; sync: { + data?: LargePrefHelper; lock: boolean; manager: { window?: Window; @@ -73,15 +75,18 @@ class Addon { top: number; }; }; - templateEditor: { - window?: Window; - tableHelper?: VirtualizedTableHelper; - editor?: any; - templates: { name: string }[]; - }; - templatePicker: { - mode: "insert" | "create" | "export"; - data: Record; + template: { + data?: LargePrefHelper; + editor: { + window?: Window; + tableHelper?: VirtualizedTableHelper; + editor?: any; + templates: string[]; + }; + picker: { + mode: "insert" | "create" | "export"; + data: Record; + }; }; readonly prompt?: Prompt; } = { @@ -141,14 +146,16 @@ class Addon { pined: false, anchorPosition: undefined, }, - templateEditor: { - window: undefined, - tableHelper: undefined, - templates: [], - }, - templatePicker: { - mode: "insert", - data: {}, + template: { + editor: { + window: undefined, + tableHelper: undefined, + templates: [], + }, + picker: { + mode: "insert", + data: {}, + }, }, get prompt() { return ToolkitGlobal.getInstance().prompt.instance; diff --git a/src/api.ts b/src/api.ts index bbdfbf3..dfba894 100644 --- a/src/api.ts +++ b/src/api.ts @@ -38,7 +38,6 @@ import { getTemplateKeys, getTemplateText, setTemplate, - initTemplates, removeTemplate, } from "./modules/template/controller"; import { @@ -103,7 +102,6 @@ const template = { getTemplateKeys, getTemplateText, setTemplate, - initTemplates, removeTemplate, renderTemplatePreview, }; diff --git a/src/hooks.ts b/src/hooks.ts index 4ac2ead..ba6a7b6 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -49,6 +49,7 @@ import { import { annotationTagAction } from "./modules/annotationTagAction"; import { createZToolkit } from "./utils/ztoolkit"; import { waitUtilAsync } from "./utils/wait"; +import { initSyncList } from "./modules/sync/api"; async function onStartup() { await Promise.all([ @@ -68,6 +69,8 @@ async function onStartup() { registerPrefsWindow(); + initSyncList(); + setSyncing(); await onMainWindowLoad(window); diff --git a/src/modules/sync/api.ts b/src/modules/sync/api.ts index b912fee..96684c5 100644 --- a/src/modules/sync/api.ts +++ b/src/modules/sync/api.ts @@ -1,10 +1,11 @@ import YAML = require("yamljs"); -import { clearPref, getPref, setPref } from "../../utils/prefs"; +import { getPref, setPref } from "../../utils/prefs"; import { getNoteLinkParams } from "../../utils/link"; import { config } from "../../../package.json"; import { fileExists, formatPath } from "../../utils/str"; export { + initSyncList, getRelatedNoteIds, removeSyncNote, isSyncNote, @@ -18,16 +19,31 @@ export { getMDFileName, }; +function initSyncList() { + const rawKeys = getPref("syncNoteIds") as string; + if (!rawKeys.startsWith("[") || !rawKeys.endsWith("]")) { + const keys = rawKeys.split(",").map((id) => parseInt(id)); + setPref("syncNoteIds", JSON.stringify(keys)); + } + addon.data.sync.data = new ztoolkit.LargePref( + `${config.prefsPrefix}.syncNoteIds`, + `${config.prefsPrefix}.syncDetail-`, + "parser", + ); +} + function getSyncNoteIds(): number[] { - const ids = getPref("syncNoteIds") as string; - return Zotero.Items.get(ids.split(",").map((id: string) => Number(id))) + const keys = addon.data.sync.data?.getKeys(); + if (!keys) { + return []; + } + return Zotero.Items.get(keys) .filter((item) => item.isNote()) .map((item) => item.id); } function isSyncNote(noteId: number): boolean { - const syncNoteIds = getSyncNoteIds(); - return syncNoteIds.includes(noteId); + return !!addon.data.sync.data?.hasKey(String(noteId)); } async function getRelatedNoteIds(noteId: number): Promise { @@ -49,34 +65,16 @@ async function getRelatedNoteIds(noteId: number): Promise { return allNoteIds; } -async function getRelatedNoteIdsFromNotes( - noteIds: number[], -): Promise { - let allNoteIds: number[] = []; - for (const noteId of noteIds) { - allNoteIds = allNoteIds.concat(await getRelatedNoteIds(noteId)); - } - return allNoteIds; -} - function addSyncNote(noteId: number) { - const ids = getSyncNoteIds(); - if (ids.includes(noteId)) { - return; - } - ids.push(noteId); - setPref("syncNoteIds", ids.join(",")); + addon.data.sync.data?.setKey(String(noteId)); } function removeSyncNote(noteId: number) { - const ids = getSyncNoteIds(); - setPref("syncNoteIds", ids.filter((id) => id !== noteId).join(",")); - clearPref(`syncDetail-${noteId}`); + addon.data.sync.data?.deleteKey(String(noteId)); } function updateSyncStatus(noteId: number, status: SyncStatus) { - addSyncNote(noteId); - setPref(`syncDetail-${noteId}`, JSON.stringify(status)); + addon.data.sync.data?.setValue(String(noteId), status); } function getNoteStatus(noteId: number) { @@ -109,17 +107,18 @@ function getNoteStatus(noteId: number) { } function getSyncStatus(noteId?: number): SyncStatus { - const defaultStatus = JSON.stringify({ + const defaultStatus = { path: "", filename: "", md5: "", noteMd5: "", lastsync: new Date().getTime(), itemID: -1, - }); - const status = JSON.parse( - (getPref(`syncDetail-${noteId}`) as string) || defaultStatus, - ); + }; + const status = { + ...defaultStatus, + ...(addon.data.sync.data?.getValue(String(noteId)) as SyncStatus), + }; status.path = formatPath(status.path); return status; } diff --git a/src/modules/template/api.ts b/src/modules/template/api.ts index 8c04071..762fdb9 100644 --- a/src/modules/template/api.ts +++ b/src/modules/template/api.ts @@ -210,7 +210,7 @@ async function runItemTemplate( } async function getItemTemplateData() { - const librarySelectedIds = addon.data.templatePicker.data.librarySelectedIds; + const librarySelectedIds = addon.data.template.picker.data.librarySelectedIds; if (librarySelectedIds && librarySelectedIds.length !== 0) { const firstSelectedItem = Zotero.Items.get(librarySelectedIds[0]); const data = {} as Record; diff --git a/src/modules/template/controller.ts b/src/modules/template/controller.ts index 04eef92..52811be 100644 --- a/src/modules/template/controller.ts +++ b/src/modules/template/controller.ts @@ -1,6 +1,7 @@ import YAML = require("yamljs"); -import { clearPref, getPref, setPref } from "../../utils/prefs"; +import { getPref } from "../../utils/prefs"; import { showHint } from "../../utils/hint"; +import { config } from "../../../package.json"; export { getTemplateKeys, @@ -11,52 +12,46 @@ export { importTemplateFromClipboard, }; -// Controller -function getTemplateKeys(): { name: string }[] { - const templateKeys = getPref("templateKeys") as string; - return templateKeys ? JSON.parse(templateKeys) : []; -} - -function setTemplateKeys(templateKeys: { name: string }[]): void { - setPref("templateKeys", JSON.stringify(templateKeys)); -} - -function addTemplateKey(templateKey: { name: string }): boolean { - const templateKeys = getTemplateKeys(); - if (templateKeys.map((t) => t.name).includes(templateKey.name)) { - return false; +function initTemplates() { + addon.data.template.data = new ztoolkit.LargePref( + `${config.prefsPrefix}.templateKeys`, + `${config.prefsPrefix}.template.`, + "parser", + ); + // Convert old template keys to new format + const raw = getPref("templateKeys") as string; + let keys = raw ? JSON.parse(raw) : []; + if (keys.length > 0 && typeof keys[0] !== "string") { + keys = keys.map((t: { name: string }) => t.name); + setTemplateKeys(keys); } - templateKeys.push(templateKey); - setTemplateKeys(templateKeys); - return true; + // Add default templates + const templateKeys = getTemplateKeys(); + for (const defaultTemplate of addon.api.template.DEFAULT_TEMPLATES) { + if (!templateKeys.includes(defaultTemplate.name)) { + setTemplate(defaultTemplate, false); + } + } + addon.hooks.onUpdateTemplatePicker(); } -function removeTemplateKey(keyName: string): boolean { - const templateKeys = getTemplateKeys(); - if (!templateKeys.map((t) => t.name).includes(keyName)) { - return false; - } - templateKeys.splice(templateKeys.map((t) => t.name).indexOf(keyName), 1); - setTemplateKeys(templateKeys); - return true; +function getTemplateKeys(): string[] { + return addon.data.template.data?.getKeys() || []; +} + +function setTemplateKeys(templateKeys: string[]): void { + addon.data.template.data?.setKeys(templateKeys); } function getTemplateText(keyName: string): string { - let template = getPref(`template.${keyName}`) as string; - if (!template) { - template = ""; - setPref(`template.${keyName}`, template); - } - return template; + return addon.data.template.data?.getValue(keyName) || ""; } function setTemplate( template: NoteTemplate, updatePrompt: boolean = true, ): void { - template = JSON.parse(JSON.stringify(template)); - addTemplateKey({ name: template.name }); - setPref(`template.${template.name}`, template.text); + addon.data.template.data?.setValue(template.name, template.text); if (updatePrompt) { addon.hooks.onUpdateTemplatePicker(); } @@ -66,27 +61,15 @@ function removeTemplate( keyName: string | undefined, updatePrompt: boolean = true, ): void { - if (typeof keyName === "undefined") { + if (!keyName) { return; } - removeTemplateKey(keyName); - clearPref(`template.${keyName}`); + addon.data.template.data?.deleteKey(keyName); if (updatePrompt) { addon.hooks.onUpdateTemplatePicker(); } } -function initTemplates() { - const templateKeys = getTemplateKeys(); - const currentNames = templateKeys.map((t) => t.name); - for (const defaultTemplate of addon.api.template.DEFAULT_TEMPLATES) { - if (!currentNames.includes(defaultTemplate.name)) { - setTemplate(defaultTemplate, false); - } - } - addon.hooks.onUpdateTemplatePicker(); -} - function importTemplateFromClipboard() { const templateText = Zotero.Utilities.Internal.getClipboard("text/unicode"); if (!templateText) { diff --git a/src/modules/template/editorWindow.ts b/src/modules/template/editorWindow.ts index 8a94af4..8030694 100644 --- a/src/modules/template/editorWindow.ts +++ b/src/modules/template/editorWindow.ts @@ -7,9 +7,9 @@ import { waitUtilAsync } from "../../utils/wait"; export async function showTemplateEditor() { if ( - !addon.data.templateEditor.window || - Components.utils.isDeadWrapper(addon.data.templateEditor.window) || - addon.data.templateEditor.window.closed + !addon.data.template.editor.window || + Components.utils.isDeadWrapper(addon.data.template.editor.window) || + addon.data.template.editor.window.closed ) { const windowArgs = { _initPromise: Zotero.Promise.defer(), @@ -20,10 +20,10 @@ export async function showTemplateEditor() { `chrome,centerscreen,resizable,status,width=600,height=400,dialog=no`, windowArgs, )!; - addon.data.templateEditor.window = _window; + addon.data.template.editor.window = _window; await windowArgs._initPromise.promise; updateData(); - addon.data.templateEditor.tableHelper = new ztoolkit.VirtualizedTable( + addon.data.template.editor.tableHelper = new ztoolkit.VirtualizedTable( _window!, ) .setContainerId("table-container") @@ -47,14 +47,10 @@ export async function showTemplateEditor() { staticColumns: true, disableFontSizeScaling: true, }) - .setProp("getRowCount", () => addon.data.templateEditor.templates.length) - .setProp( - "getRowData", - (index) => - (addon.data.templateEditor.templates[index] as { name: string }) || { - name: "no data", - }, - ) + .setProp("getRowCount", () => addon.data.template.editor.templates.length) + .setProp("getRowData", (index) => ({ + name: addon.data.template.editor.templates[index] || "no data", + })) .setProp("onSelectionChange", (selection) => { updateEditor(); updatePreview(); @@ -128,7 +124,7 @@ export async function showTemplateEditor() { ?.addEventListener("click", (ev) => { restoreTemplates(_window); }); - addon.data.templateEditor.window?.focus(); + addon.data.template.editor.window?.focus(); const editorWin = (_window.document.querySelector("#editor") as any) .contentWindow; await waitUtilAsync(() => editorWin?.loadMonaco); @@ -136,7 +132,7 @@ export async function showTemplateEditor() { language: "javascript", theme: "vs-light", }); - addon.data.templateEditor.editor = editor; + addon.data.template.editor.editor = editor; } } @@ -148,33 +144,33 @@ async function refresh() { } function updateData() { - addon.data.templateEditor.templates = addon.api.template.getTemplateKeys(); + addon.data.template.editor.templates = addon.api.template.getTemplateKeys(); } function updateTable(selectId?: number) { - addon.data.templateEditor.tableHelper?.render(selectId); + addon.data.template.editor.tableHelper?.render(selectId); } function updateEditor() { const name = getSelectedTemplateName(); const templateText = addon.api.template.getTemplateText(name); - const header = addon.data.templateEditor.window?.document.getElementById( + const header = addon.data.template.editor.window?.document.getElementById( "editor-name", ) as HTMLInputElement; - const editor = addon.data.templateEditor.window?.document.getElementById( + const editor = addon.data.template.editor.window?.document.getElementById( "editor", ) as HTMLIFrameElement; const saveTemplate = - addon.data.templateEditor.window?.document.getElementById( + addon.data.template.editor.window?.document.getElementById( "save", ) as XUL.Button; const deleteTemplate = - addon.data.templateEditor.window?.document.getElementById( + addon.data.template.editor.window?.document.getElementById( "delete", ) as XUL.Button; const resetTemplate = - addon.data.templateEditor.window?.document.getElementById( + addon.data.template.editor.window?.document.getElementById( "reset", ) as XUL.Button; if (!name) { @@ -197,7 +193,7 @@ function updateEditor() { deleteTemplate.hidden = true; resetTemplate.hidden = false; } - addon.data.templateEditor.editor.setValue(templateText); + addon.data.template.editor.editor.setValue(templateText); editor.hidden = false; saveTemplate.removeAttribute("disabled"); deleteTemplate.removeAttribute("disabled"); @@ -211,7 +207,7 @@ async function updatePreview() { .replace(/
/g, "
") .replace(/
/g, "
") .replace(/]+)>/g, ""); - const win = addon.data.templateEditor.window; + const win = addon.data.template.editor.window; const container = win?.document.getElementById("preview-container"); if (container) { container.innerHTML = html; @@ -219,13 +215,13 @@ async function updatePreview() { } function getSelectedTemplateName() { - const selectedTemplate = addon.data.templateEditor.templates.find( + const selectedTemplate = addon.data.template.editor.templates.find( (v, i) => - addon.data.templateEditor.tableHelper?.treeInstance.selection.isSelected( + addon.data.template.editor.tableHelper?.treeInstance.selection.isSelected( i, ), ); - return selectedTemplate?.name || ""; + return selectedTemplate || ""; } function createTemplate() { @@ -255,7 +251,7 @@ async function importNoteTemplate() { function saveSelectedTemplate() { const name = getSelectedTemplateName(); - const header = addon.data.templateEditor.window?.document.getElementById( + const header = addon.data.template.editor.window?.document.getElementById( "editor-name", ) as HTMLInputElement; @@ -271,7 +267,7 @@ function saveSelectedTemplate() { const template = { name: header.value, - text: addon.data.templateEditor.editor.getValue(), + text: addon.data.template.editor.editor.getValue(), }; addon.api.template.setTemplate(template); if (name !== template.name) { @@ -279,7 +275,7 @@ function saveSelectedTemplate() { } showHint(`Template ${template.name} saved.`); const selectedId = - addon.data.templateEditor.tableHelper?.treeInstance.selection.selected + addon.data.template.editor.tableHelper?.treeInstance.selection.selected .values() .next().value; refresh().then(() => updateTable(selectedId)); @@ -300,7 +296,7 @@ function deleteSelectedTemplate() { function resetSelectedTemplate() { const name = getSelectedTemplateName(); if (addon.api.template.SYSTEM_TEMPLATE_NAMES.includes(name)) { - addon.data.templateEditor.editor.setValue( + addon.data.template.editor.editor.setValue( addon.api.template.DEFAULT_TEMPLATES.find((t) => t.name === name)?.text || "", ); @@ -343,7 +339,7 @@ async function backupTemplates() { if (!filepath) { return; } - const keys = addon.api.template.getTemplateKeys().map((t) => t.name); + const keys = addon.api.template.getTemplateKeys(); const templates = keys.map((key) => { return { name: key, @@ -368,7 +364,7 @@ async function restoreTemplates(win: Window) { } const yaml = (await Zotero.File.getContentsAsync(filepath)) as string; const templates = YAML.parse(yaml) as NoteTemplate[]; - const existingNames = addon.api.template.getTemplateKeys().map((t) => t.name); + const existingNames = addon.api.template.getTemplateKeys(); for (const t of templates) { if (existingNames.includes(t.name)) { diff --git a/src/modules/template/picker.ts b/src/modules/template/picker.ts index 4bd2677..0ddbec8 100644 --- a/src/modules/template/picker.ts +++ b/src/modules/template/picker.ts @@ -15,12 +15,12 @@ function showTemplatePicker( function showTemplatePicker(mode: "export", data?: Record): void; function showTemplatePicker(): void; function showTemplatePicker( - mode: typeof addon.data.templatePicker.mode = "insert", + mode: typeof addon.data.template.picker.mode = "insert", data: Record = {}, ) { if (addon.data.prompt) { - addon.data.templatePicker.mode = mode; - addon.data.templatePicker.data = data; + addon.data.template.picker.mode = mode; + addon.data.template.picker.data = data; addon.data.prompt.promptNode.style.display = "flex"; addon.data.prompt.showCommands( addon.data.prompt.commands.filter( @@ -32,18 +32,18 @@ function showTemplatePicker( function updateTemplatePicker() { ztoolkit.Prompt.unregisterAll(); - const templates = addon.api.template.getTemplateKeys(); + const templateKeys = addon.api.template.getTemplateKeys(); ztoolkit.Prompt.register( - templates + templateKeys .filter( (template) => - !addon.api.template.SYSTEM_TEMPLATE_NAMES.includes(template.name), + !addon.api.template.SYSTEM_TEMPLATE_NAMES.includes(template), ) .map((template) => { return { - name: `Template: ${template.name}`, + name: `Template: ${template}`, label: "BNotes Template", - callback: getTemplatePromptHandler(template.name), + callback: getTemplatePromptHandler(template), }; }), ); @@ -54,7 +54,7 @@ function getTemplatePromptHandler(name: string) { ztoolkit.log(prompt, name); prompt.promptNode.style.display = "none"; // TODO: add preview when command is selected - switch (addon.data.templatePicker.mode) { + switch (addon.data.template.picker.mode) { case "create": await createTemplateNoteCallback(name); break; @@ -66,14 +66,14 @@ function getTemplatePromptHandler(name: string) { await insertTemplateCallback(name); break; } - addon.data.templatePicker.mode = "insert"; - addon.data.templatePicker.data = {}; + addon.data.template.picker.mode = "insert"; + addon.data.template.picker.data = {}; }; } async function insertTemplateCallback(name: string) { const targetNoteItem = Zotero.Items.get( - addon.data.templatePicker.data.noteId || addon.data.workspace.mainId, + addon.data.template.picker.data.noteId || addon.data.workspace.mainId, ); let html = ""; if (name.toLowerCase().startsWith("[item]")) { @@ -88,14 +88,14 @@ async function insertTemplateCallback(name: string) { await addLineToNote( targetNoteItem, html, - addon.data.templatePicker.data.lineIndex, + addon.data.template.picker.data.lineIndex, ); } async function createTemplateNoteCallback(name: string) { - addon.data.templatePicker.data.librarySelectedIds = + addon.data.template.picker.data.librarySelectedIds = ZoteroPane.getSelectedItems(true); - switch (addon.data.templatePicker.data.noteType) { + switch (addon.data.template.picker.data.noteType) { case "standalone": { const currentCollection = ZoteroPane.getSelectedCollection(); if (!currentCollection) { @@ -105,16 +105,16 @@ async function createTemplateNoteCallback(name: string) { const noteID = await ZoteroPane.newNote(); const noteItem = Zotero.Items.get(noteID); await noteItem.saveTx(); - addon.data.templatePicker.data.noteId = noteID; + addon.data.template.picker.data.noteId = noteID; break; } case "item": { - const parentID = addon.data.templatePicker.data.parentItemId; + const parentID = addon.data.template.picker.data.parentItemId; const noteItem = new Zotero.Item("note"); noteItem.libraryID = Zotero.Items.get(parentID).libraryID; noteItem.parentID = parentID; await noteItem.saveTx(); - addon.data.templatePicker.data.noteId = noteItem.id; + addon.data.template.picker.data.noteId = noteItem.id; break; } default: @@ -124,13 +124,13 @@ async function createTemplateNoteCallback(name: string) { } async function exportTemplateCallback(name: string) { - addon.data.templatePicker.data.librarySelectedIds = + addon.data.template.picker.data.librarySelectedIds = ZoteroPane.getSelectedItems(true); // Create temp note const noteItem = new Zotero.Item("note"); noteItem.libraryID = Zotero.Libraries.userLibraryID; await noteItem.saveTx(); - addon.data.templatePicker.data.noteId = noteItem.id; + addon.data.template.picker.data.noteId = noteItem.id; await insertTemplateCallback(name); // Export note await addon.hooks.onShowExportNoteOptions([noteItem.id], { diff --git a/src/utils/ztoolkit.ts b/src/utils/ztoolkit.ts index 8bf0bde..e995e06 100644 --- a/src/utils/ztoolkit.ts +++ b/src/utils/ztoolkit.ts @@ -40,6 +40,7 @@ import { MenuManager } from "zotero-plugin-toolkit/dist/managers/menu"; import { PromptManager } from "zotero-plugin-toolkit/dist/managers/prompt"; import { ReaderInstanceManager } from "zotero-plugin-toolkit/dist/managers/readerInstance"; import { ReaderTabPanelManager } from "zotero-plugin-toolkit/dist/managers/readerTabPanel"; +import { LargePrefHelper } from "zotero-plugin-toolkit/dist/helpers/largePref"; class MyToolkit extends BasicTool { UI: UITool; @@ -54,6 +55,7 @@ class MyToolkit extends BasicTool { ProgressWindow: typeof ProgressWindowHelper; VirtualizedTable: typeof VirtualizedTableHelper; Dialog: typeof DialogHelper; + LargePref: typeof LargePrefHelper; constructor() { super(); @@ -69,6 +71,7 @@ class MyToolkit extends BasicTool { this.ProgressWindow = ProgressWindowHelper; this.VirtualizedTable = VirtualizedTableHelper; this.Dialog = DialogHelper; + this.LargePref = LargePrefHelper; } unregisterAll() {