diff --git a/addon/chrome/content/export.xul b/addon/chrome/content/export.xul
index 6807a94..1de4dad 100644
--- a/addon/chrome/content/export.xul
+++ b/addon/chrome/content/export.xul
@@ -18,7 +18,7 @@
-
+
@@ -31,13 +31,17 @@
-
+
+
+
+
+
diff --git a/addon/chrome/content/sync.xul b/addon/chrome/content/sync.xul
new file mode 100644
index 0000000..1e66191
--- /dev/null
+++ b/addon/chrome/content/sync.xul
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+ %ZoteroDTD;
+
+ %knowledgeDTD;
+]>
+
+
\ No newline at end of file
diff --git a/addon/chrome/locale/en-US/overlay.dtd b/addon/chrome/locale/en-US/overlay.dtd
index 5871031..7690a05 100644
--- a/addon/chrome/locale/en-US/overlay.dtd
+++ b/addon/chrome/locale/en-US/overlay.dtd
@@ -30,11 +30,19 @@
+
+
+
+
+
+
+
+
diff --git a/addon/chrome/locale/zh-CN/overlay.dtd b/addon/chrome/locale/zh-CN/overlay.dtd
index f107079..404929a 100644
--- a/addon/chrome/locale/zh-CN/overlay.dtd
+++ b/addon/chrome/locale/zh-CN/overlay.dtd
@@ -30,11 +30,19 @@
+
+
+
+
+
+
+
+
diff --git a/src/Better Note Markdown.js b/src/Better Note Markdown.js
index c594665..fab174f 100644
--- a/src/Better Note Markdown.js
+++ b/src/Better Note Markdown.js
@@ -1734,8 +1734,13 @@ let bundle;
Zotero.debug(newAbsPath);
let newFile = oldFile;
try {
- newFile = _Zotero.File.copyToUnique(oldFile, newAbsPath).path;
- newFile = newFile.replace(/\\/g, "/");
+ // Don't overwrite
+ if (await OS.File.exists(newAbsPath)) {
+ newFile = newAbsPath.replace(/\\/g, "/");
+ } else {
+ newFile = _Zotero.File.copyToUnique(oldFile, newAbsPath).path;
+ newFile = newFile.replace(/\\/g, "/");
+ }
newFile = `attachments/${newFile.split(/\//).pop()}`;
} catch (e) {
Zotero.debug(e);
diff --git a/src/addon.ts b/src/addon.ts
index 9cccdba..cff1be8 100644
--- a/src/addon.ts
+++ b/src/addon.ts
@@ -4,12 +4,14 @@ import AddonWizard from "./wizard";
import AddonExport from "./export";
import Knowledge from "./knowledge";
import AddonTemplate from "./template";
+import AddonSync from "./sync";
class Knowledge4Zotero {
public events: AddonEvents;
public views: AddonViews;
public wizard: AddonWizard;
public export: AddonExport;
+ public sync: AddonSync;
public template: AddonTemplate;
public knowledge: Knowledge;
@@ -18,6 +20,7 @@ class Knowledge4Zotero {
this.views = new AddonViews(this);
this.wizard = new AddonWizard(this);
this.export = new AddonExport(this);
+ this.sync = new AddonSync(this);
this.template = new AddonTemplate(this);
this.knowledge = new Knowledge(this);
}
diff --git a/src/events.ts b/src/events.ts
index a81716e..23b7ef7 100644
--- a/src/events.ts
+++ b/src/events.ts
@@ -19,6 +19,12 @@ class AddonEvents extends AddonBase {
Zotero.debug("Knowledge4Zotero: main knowledge modify check.");
this._Addon.views.updateOutline();
}
+ // Check Note Sync
+ const syncIds = this._Addon.sync.getSyncNoteIds();
+ if (ids.filter((id) => syncIds.includes(id)).length > 0) {
+ this._Addon.sync.setSync();
+ Zotero.debug("Better Notes: sync planned.");
+ }
}
if (
(event == "select" &&
@@ -118,6 +124,9 @@ class AddonEvents extends AddonBase {
this._Addon.views.switchKey(true);
this.initItemSelectListener();
+
+ // Set a init sync
+ this._Addon.sync.setSync();
}
private async initWorkspaceTab() {
@@ -1292,6 +1301,25 @@ class AddonEvents extends AddonBase {
if (!item) {
item = this._Addon.knowledge.getWorkspaceNote();
}
+ // If this note is in sync list, open sync window
+ if (this._Addon.sync.getSyncNoteIds().includes(item.id)) {
+ const io = {
+ dataIn: item,
+ dataOut: {} as any,
+ deferred: Zotero.Promise.defer(),
+ };
+
+ (window as unknown as XULWindow).openDialog(
+ "chrome://Knowledge4Zotero/content/sync.xul",
+ "",
+ "chrome,centerscreen,width=500,height=200",
+ io
+ );
+ await io.deferred.promise;
+ if (!io.dataOut.export) {
+ return;
+ }
+ }
const io = {
dataIn: null,
dataOut: null,
@@ -1301,14 +1329,18 @@ class AddonEvents extends AddonBase {
(window as unknown as XULWindow).openDialog(
"chrome://Knowledge4Zotero/content/export.xul",
"",
- "chrome,centerscreen,width=300,height=450",
+ "chrome,centerscreen,width=400,height=400",
io
);
await io.deferred.promise;
const options = io.dataOut;
if (options.exportFile && options.exportSingleFile) {
- await this._Addon.knowledge.exportNotesToFile([item], false);
+ await this._Addon.knowledge.exportNotesToFile(
+ [item],
+ false,
+ options.exportAutoSync
+ );
} else {
await this._Addon.knowledge.exportNoteToFile(
item,
@@ -1545,6 +1577,8 @@ class AddonEvents extends AddonBase {
this._Addon.template.resetTemplates();
// Initialize citation style
this._Addon.template.getCitationStyle();
+ // Initialize sync notes
+ this._Addon.sync.getSyncNoteIds();
}
}
diff --git a/src/export.ts b/src/export.ts
index 3bc1806..41d315a 100644
--- a/src/export.ts
+++ b/src/export.ts
@@ -33,6 +33,14 @@ class AddonExport extends AddonBase {
) as XUL.Checkbox
).checked = exportSingleFile;
}
+ let exportAutoSync = Zotero.Prefs.get("Knowledge4Zotero.exportAutoSync");
+ if (typeof exportAutoSync !== "undefined") {
+ (
+ this._window.document.getElementById(
+ "Knowledge4Zotero-export-enableautosync"
+ ) as XUL.Checkbox
+ ).checked = exportAutoSync;
+ }
let embedLink = Zotero.Prefs.get("Knowledge4Zotero.embedLink");
if (typeof embedLink !== "undefined") {
(
@@ -67,26 +75,49 @@ class AddonExport extends AddonBase {
}
this.doUpdate();
}
- doUpdate() {
- (
- this._window.document.getElementById(
- "Knowledge4Zotero-export-embedLink"
- ) as XUL.Checkbox
- ).disabled = (
- this._window.document.getElementById(
- "Knowledge4Zotero-export-enablesingle"
- ) as XUL.Checkbox
- ).checked;
+ doUpdate(event: XULEvent = undefined) {
+ let embedLink = this._window.document.getElementById(
+ "Knowledge4Zotero-export-embedLink"
+ ) as XUL.Checkbox;
- (
- this._window.document.getElementById(
- "Knowledge4Zotero-export-enablesingle"
- ) as XUL.Checkbox
- ).disabled = !(
- this._window.document.getElementById(
- "Knowledge4Zotero-export-enablefile"
- ) as XUL.Checkbox
- ).checked;
+ let exportFile = this._window.document.getElementById(
+ "Knowledge4Zotero-export-enablefile"
+ ) as XUL.Checkbox;
+ let exportSingleFile = this._window.document.getElementById(
+ "Knowledge4Zotero-export-enablesingle"
+ ) as XUL.Checkbox;
+ let exportAutoSync = this._window.document.getElementById(
+ "Knowledge4Zotero-export-enableautosync"
+ ) as XUL.Checkbox;
+
+ if (event) {
+ if (
+ event.target.id === "Knowledge4Zotero-export-embedLink" &&
+ embedLink.checked
+ ) {
+ exportSingleFile.checked = false;
+ exportAutoSync.checked = false;
+ }
+ if (event.target.id === "Knowledge4Zotero-export-enablesingle") {
+ if (exportSingleFile.checked) {
+ embedLink.checked = false;
+ } else {
+ exportAutoSync.checked = false;
+ }
+ }
+ }
+
+ if (exportFile.checked && !embedLink.checked) {
+ exportSingleFile.disabled = false;
+ } else {
+ exportSingleFile.disabled = true;
+ }
+
+ if (exportFile.checked && exportSingleFile.checked) {
+ exportAutoSync.disabled = false;
+ } else {
+ exportAutoSync.disabled = true;
+ }
}
doUnload() {
this.io.deferred && this.io.deferred.resolve();
@@ -103,6 +134,11 @@ class AddonExport extends AddonBase {
"Knowledge4Zotero-export-enablesingle"
) as XUL.Checkbox
).checked;
+ let exportAutoSync = (
+ this._window.document.getElementById(
+ "Knowledge4Zotero-export-enableautosync"
+ ) as XUL.Checkbox
+ ).checked;
let embedLink = (
this._window.document.getElementById(
"Knowledge4Zotero-export-embedLink"
@@ -125,6 +161,7 @@ class AddonExport extends AddonBase {
).checked;
Zotero.Prefs.set("Knowledge4Zotero.exportFile", exportFile);
Zotero.Prefs.set("Knowledge4Zotero.exportSingleFile", exportSingleFile);
+ Zotero.Prefs.set("Knowledge4Zotero.exportAutoSync", exportAutoSync);
Zotero.Prefs.set("Knowledge4Zotero.embedLink", embedLink);
Zotero.Prefs.set("Knowledge4Zotero.exportNote", exportNote);
Zotero.Prefs.set("Knowledge4Zotero.exportCopy", exportCopy);
@@ -134,6 +171,7 @@ class AddonExport extends AddonBase {
this.io.dataOut = {
exportFile: exportFile,
exportSingleFile: exportSingleFile,
+ exportAutoSync: exportAutoSync,
embedLink: embedLink,
exportNote: exportNote,
exportCopy: exportCopy,
diff --git a/src/knowledge.ts b/src/knowledge.ts
index 926b223..af104de 100644
--- a/src/knowledge.ts
+++ b/src/knowledge.ts
@@ -860,7 +860,11 @@ class Knowledge extends AddonBase {
}
}
- async exportNotesToFile(notes: ZoteroItem[], useEmbed: boolean) {
+ async exportNotesToFile(
+ notes: ZoteroItem[],
+ useEmbed: boolean,
+ useSync: boolean = false
+ ) {
Components.utils.import("resource://gre/modules/osfile.jsm");
this._exportFileDict = [];
const filepath = await pick(
@@ -948,15 +952,66 @@ class Knowledge extends AddonBase {
for (const noteInfo of noteLinkDict) {
this._exportNote = noteInfo.note;
- this._export(
- noteInfo.note,
- `${Zotero.File.pathToFile(filepath).path}/${noteInfo.filename}`,
- false
- );
+ let exportPath = `${Zotero.File.pathToFile(filepath).path}/${
+ noteInfo.filename
+ }`;
+ this._export(noteInfo.note, exportPath, false);
+ if (useSync) {
+ this._Addon.sync.updateNoteSyncStatus(
+ noteInfo.note,
+ Zotero.File.pathToFile(filepath).path,
+ noteInfo.filename
+ );
+ }
}
}
}
+ async syncNotesToFile(notes: ZoteroItem[], filepath: string) {
+ this._exportPath = Zotero.File.pathToFile(filepath).path + "/attachments";
+ // Convert to unix format
+ this._exportPath = this._exportPath.replace(/\\/g, "/");
+
+ // Export every linked note as a markdown file
+ // Find all linked notes that need to be exported
+ let allNoteIds: number[] = [].concat(notes.map((n) => n.id));
+ for (const note of notes) {
+ const subNoteIds = (
+ await Promise.all(
+ note
+ .getNote()
+ .match(/zotero:\/\/note\/\w+\/\w+\//g)
+ .map(async (link) => this.getNoteFromLink(link))
+ )
+ )
+ .filter((res) => res.item)
+ .map((res) => res.item.id);
+ allNoteIds = allNoteIds.concat(subNoteIds);
+ }
+ allNoteIds = new Array(...new Set(allNoteIds));
+ // console.log(allNoteIds);
+ const allNoteItems: ZoteroItem[] = Zotero.Items.get(allNoteIds);
+ const noteLinkDict = allNoteItems.map((_note) => {
+ return {
+ link: this.getNoteLink(_note),
+ id: _note.id,
+ note: _note,
+ filename: this._getFileName(_note),
+ };
+ });
+ this._exportFileDict = noteLinkDict;
+
+ for (const note of notes) {
+ this._exportNote = note;
+ const syncInfo = this._Addon.sync.getNoteSyncStatus(note);
+ let exportPath = `${decodeURIComponent(
+ syncInfo.path
+ )}/${decodeURIComponent(syncInfo.filename)}`;
+ this._export(note, exportPath, false);
+ this._Addon.sync.updateNoteSyncStatus(note);
+ }
+ }
+
private async _export(
note: ZoteroItem,
filename: string,
diff --git a/src/sync.ts b/src/sync.ts
new file mode 100644
index 0000000..49fff58
--- /dev/null
+++ b/src/sync.ts
@@ -0,0 +1,214 @@
+import { AddonBase } from "./base";
+
+class AddonSync extends AddonBase {
+ triggerTime: number;
+ private io: {
+ dataIn: any;
+ dataOut: any;
+ deferred?: typeof Promise;
+ };
+ private _window: Window;
+ constructor(parent: Knowledge4Zotero) {
+ super(parent);
+ }
+
+ doLoad(_window: Window) {
+ this._window = _window;
+ this.io = (this._window as unknown as XULWindow).arguments[0];
+ this.doUpdate();
+ }
+
+ doUpdate() {
+ const syncInfo = this.getNoteSyncStatus(this.io.dataIn);
+ this._window.document
+ .getElementById("Knowledge4Zotero-sync-path")
+ .setAttribute(
+ "value",
+ `${decodeURIComponent(syncInfo.path)}/${decodeURIComponent(
+ syncInfo.filename
+ )}`
+ );
+
+ let lastSync: string;
+ const lastSyncTime = Number(syncInfo.lastsync);
+ const currentTime = new Date().getTime();
+ if (currentTime - lastSyncTime <= 60000) {
+ lastSync = `${Math.round(
+ (currentTime - lastSyncTime) / 1000
+ )} seconds ago.`;
+ } else if (currentTime - lastSyncTime <= 3600000) {
+ lastSync = `${Math.round(
+ (currentTime - lastSyncTime) / 60000
+ )} minutes ago.`;
+ } else {
+ lastSync = new Date(lastSyncTime).toLocaleString();
+ }
+ this._window.document
+ .getElementById("Knowledge4Zotero-sync-lastsync")
+ .setAttribute("value", lastSync);
+ setTimeout(() => {
+ if (!this._window.closed) {
+ this.doUpdate();
+ }
+ }, 3000);
+ }
+
+ doUnload() {
+ this.io.deferred && this.io.deferred.resolve();
+ }
+
+ async doAccept() {
+ // Update Settings
+ let enable = (
+ this._window.document.getElementById(
+ "Knowledge4Zotero-sync-enable"
+ ) as XUL.Checkbox
+ ).checked;
+ if (!enable) {
+ const note = this.io.dataIn;
+ const allNoteIds = await this.getRelatedNoteIds(note);
+ const notes = Zotero.Items.get(allNoteIds);
+ for (const item of notes) {
+ await this.removeSyncNote(item);
+ }
+ this._Addon.views.showProgressWindow(
+ "Better Notes",
+ `Cancel sync of ${notes.length} notes.`
+ );
+ }
+ }
+ doExport() {
+ this.io.dataOut.export = true;
+ (this._window.document.querySelector("dialog") as any).acceptDialog();
+ }
+
+ getSyncNoteIds(): Number[] {
+ const ids = Zotero.Prefs.get("Knowledge4Zotero.syncNoteIds");
+ if (typeof ids === "undefined") {
+ Zotero.Prefs.set("Knowledge4Zotero.syncNoteIds", "");
+ return [];
+ }
+ return ids.split(",").map((id: string) => Number(id));
+ }
+
+ async getRelatedNoteIds(note: ZoteroItem): Promise {
+ let allNoteIds: Number[] = [note.id];
+ const subNoteIds = (
+ await Promise.all(
+ note
+ .getNote()
+ .match(/zotero:\/\/note\/\w+\/\w+\//g)
+ .map(async (link) => this._Addon.knowledge.getNoteFromLink(link))
+ )
+ )
+ .filter((res) => res.item)
+ .map((res) => res.item.id);
+ allNoteIds = allNoteIds.concat(subNoteIds);
+ allNoteIds = new Array(...new Set(allNoteIds));
+ return allNoteIds;
+ }
+
+ addSyncNote(noteItem: ZoteroItem) {
+ const ids = this.getSyncNoteIds();
+ if (ids.includes(noteItem.id)) {
+ return;
+ }
+ ids.push(noteItem.id);
+ Zotero.Prefs.set("Knowledge4Zotero.syncNoteIds", ids.join(","));
+ }
+
+ async removeSyncNote(noteItem: ZoteroItem) {
+ const ids = this.getSyncNoteIds();
+ Zotero.Prefs.set(
+ "Knowledge4Zotero.syncNoteIds",
+ ids.filter((id) => id !== noteItem.id).join(",")
+ );
+ const sycnTag = noteItem.getTags().find((t) => t.tag.includes("sync://"));
+ if (sycnTag) {
+ noteItem.removeTag(sycnTag.tag);
+ }
+ await noteItem.saveTx();
+ }
+
+ getNoteSyncStatus(noteItem: ZoteroItem): any {
+ const sycnInfo = noteItem.getTags().find((t) => t.tag.includes("sync://"));
+ if (!sycnInfo) {
+ return false;
+ }
+ const params = {};
+ sycnInfo.tag
+ .split("?")
+ .pop()
+ .split("&")
+ .forEach((p) => {
+ params[p.split("=")[0]] = p.split("=")[1];
+ });
+ return params;
+ }
+
+ async updateNoteSyncStatus(
+ noteItem: ZoteroItem,
+ path: string = "",
+ filename: string = ""
+ ) {
+ this.addSyncNote(noteItem);
+ const syncInfo = this.getNoteSyncStatus(noteItem);
+ const sycnTag = noteItem.getTags().find((t) => t.tag.includes("sync://"));
+ if (sycnTag) {
+ noteItem.removeTag(sycnTag.tag);
+ }
+ noteItem.addTag(
+ `sync://note/?version=${noteItem._version}&path=${
+ path ? encodeURIComponent(path) : syncInfo["path"]
+ }&filename=${
+ filename ? encodeURIComponent(filename) : syncInfo["filename"]
+ }&lastsync=${new Date().getTime()}`,
+ undefined
+ );
+ await noteItem.saveTx();
+ }
+
+ setSync() {
+ const _t = new Date().getTime();
+ this.triggerTime = _t;
+ setTimeout(() => {
+ if (this.triggerTime === _t) {
+ this.doSync();
+ }
+ }, 30000);
+ }
+
+ async doSync(force: boolean = false) {
+ Zotero.debug("Better Notes: sync start");
+ const items = Zotero.Items.get(this.getSyncNoteIds());
+ const toExport = {};
+ const forceNoteIds = force
+ ? await this.getRelatedNoteIds(this.io.dataIn)
+ : [];
+ for (const item of items) {
+ const syncInfo = this.getNoteSyncStatus(item);
+ const filepath = decodeURIComponent(syncInfo.path);
+ const filename = decodeURIComponent(syncInfo.filename);
+ if (
+ Number(syncInfo.version) < item._version - 1 ||
+ !(await OS.File.exists(`${filepath}/${filename}`)) ||
+ forceNoteIds.includes(item.id)
+ ) {
+ if (Object.keys(toExport).includes(filepath)) {
+ toExport[filepath].push(item);
+ } else {
+ toExport[filepath] = [item];
+ }
+ }
+ }
+ console.log(toExport);
+ for (const filepath of Object.keys(toExport)) {
+ await this._Addon.knowledge.syncNotesToFile(toExport[filepath], filepath);
+ }
+ if (this._window && !this._window.closed) {
+ this.doUpdate();
+ }
+ }
+}
+
+export default AddonSync;
diff --git a/typing/addon.d.ts b/typing/addon.d.ts
index bfbf562..011bbf4 100644
--- a/typing/addon.d.ts
+++ b/typing/addon.d.ts
@@ -3,6 +3,7 @@ declare interface Knowledge4Zotero {
views: import("../src/view");
wizard: import("../src/wizard");
export: import("../src/export");
- template:import("../src/template");
+ sync: import("../src/sync");
+ template: import("../src/template");
knowledge: import("../src/knowledge");
}
diff --git a/typing/global.d.ts b/typing/global.d.ts
index 73a7f2d..d9bc18a 100644
--- a/typing/global.d.ts
+++ b/typing/global.d.ts
@@ -92,11 +92,13 @@ declare interface ZoteroItem {
isAnnotation: () => boolean;
isPDFAttachment: () => boolean;
addTag: (name: string, type: number) => boolean;
+ removeTag(tag: string): boolean;
itemTypeID: number;
libraryID: number;
parentID: number;
parentItem: ZoteroItem;
key: string;
+ _version: any;
getField: (
name: string,
unformatted?: boolean,