add: auto sync note to markdown files

resolve: #45
This commit is contained in:
xiangyu 2022-06-22 23:29:40 +08:00
parent 95c423e231
commit 558fbd0d09
12 changed files with 430 additions and 32 deletions

View File

@ -18,7 +18,7 @@
<caption label="&zotero.__addonRef__.export.option.label;"></caption>
<rows flex="1">
<row>
<checkbox id="__addonRef__-export-embedLink" checked="true" />
<checkbox id="__addonRef__-export-embedLink" checked="true" oncommand="Zotero.Knowledge4Zotero.export.doUpdate(event)" />
<label value="&zotero.__addonRef__.export.link.enable.label;" />
</row>
<row>
@ -31,13 +31,17 @@
<caption label="&zotero.__addonRef__.export.markdown.label;"></caption>
<rows flex="1">
<row>
<checkbox id="__addonRef__-export-enablefile" checked="true" oncommand="Zotero.Knowledge4Zotero.export.doUpdate(event)" />
<checkbox id="__addonRef__-export-enablefile" checked="true" oncommand="Zotero.Knowledge4Zotero.export.doUpdate(event)" />
<label value="&zotero.__addonRef__.export.file.enable.label;" />
</row>
<row>
<checkbox id="__addonRef__-export-enablesingle" checked="false" oncommand="Zotero.Knowledge4Zotero.export.doUpdate(event)" />
<label value="&zotero.__addonRef__.export.singlefile.enable.label;" />
</row>
<row>
<checkbox id="__addonRef__-export-enableautosync" checked="false" oncommand="Zotero.Knowledge4Zotero.export.doUpdate(event)" />
<label value="&zotero.__addonRef__.export.enableautosync.enable.label;" />
</row>
</rows>
</groupbox>
<groupbox flex="1">

View File

@ -0,0 +1,26 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/"?>
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/overlay.css"?>
<!DOCTYPE window [
<!ENTITY % ZoteroDTD SYSTEM "chrome://zotero/locale/zotero.dtd">
%ZoteroDTD;
<!ENTITY % knowledgeDTD SYSTEM "chrome://__addonRef__/locale/overlay.dtd">
%knowledgeDTD;
]>
<dialog id="betternotes-sync-dialog" windowtype="betternotes-sync" title="&zotero.__addonRef__.sync.title;" orient="vertical" width="300" height="300" buttons="cancel,accept,help" ondialogaccept="Zotero.Knowledge4Zotero.sync.doAccept();" ondialoghelp="Zotero.Knowledge4Zotero.sync.doExport();" onload="Zotero.Knowledge4Zotero.sync.doLoad(window);" onunload="Zotero.Knowledge4Zotero.sync.doUnload();" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml" style="padding:2em" persist="screenX screenY width height" buttonlabelhelp="&zotero.__addonRef__.sync.export.label;">
<script src="chrome://zotero/content/include.js" />
<vbox flex="1">
<label value="&zotero.__addonRef__.sync.path.label;" />
<label id="__addonRef__-sync-path" />
<label value="&zotero.__addonRef__.sync.lastsync.label;" />
<label id="__addonRef__-sync-lastsync" />
<hbox>
<checkbox id="__addonRef__-sync-enable" label="&zotero.__addonRef__.sync.enable.label;" checked="true" />
<button flex="0" label="&zotero.__addonRef__.sync.dosync.label;" oncommand="Zotero.Knowledge4Zotero.sync.doSync(true)"></button>
</hbox>
</vbox>
</dialog>

View File

@ -30,11 +30,19 @@
<!ENTITY zotero.__addonRef__.export.markdown.label "MarkDown Settings">
<!ENTITY zotero.__addonRef__.export.file.enable.label "Export to MarkDown File">
<!ENTITY zotero.__addonRef__.export.singlefile.enable.label "Export Linked Notes to MarkDown File">
<!ENTITY zotero.__addonRef__.export.enableautosync.enable.label "Auto Sync to Export Path">
<!ENTITY zotero.__addonRef__.export.richtext.label "RichText(MS Word) Settings">
<!ENTITY zotero.__addonRef__.export.copy.enable.label "Export to clipboard">
<!ENTITY zotero.__addonRef__.export.pdf.label "PDF Settings">
<!ENTITY zotero.__addonRef__.export.pdf.enable.label "Export to PDF">
<!ENTITY zotero.__addonRef__.sync.title "Sync Status">
<!ENTITY zotero.__addonRef__.sync.export.label "Export to...">
<!ENTITY zotero.__addonRef__.sync.path.label "Sync to:">
<!ENTITY zotero.__addonRef__.sync.lastsync.label "Last sync:">
<!ENTITY zotero.__addonRef__.sync.enable.label "Keep in sync">
<!ENTITY zotero.__addonRef__.sync.dosync.label "Sync Now">
<!ENTITY zotero.__addonRef__.wizard.title "Welcomed to Zotero Better Notes">
<!ENTITY zotero.__addonRef__.wizard.page1.header "Zotero Better Notes User Guide">
<!ENTITY zotero.__addonRef__.wizard.page1.description "Click Next to Continue.">

View File

@ -30,11 +30,19 @@
<!ENTITY zotero.__addonRef__.export.markdown.label "MarkDown设置">
<!ENTITY zotero.__addonRef__.export.file.enable.label "导出为MarkDown文件">
<!ENTITY zotero.__addonRef__.export.singlefile.enable.label "导出链接的子笔记为MarkDown文件">
<!ENTITY zotero.__addonRef__.export.enableautosync.enable.label "修改时自动同步到导出路径">
<!ENTITY zotero.__addonRef__.export.richtext.label "富文本(MS Word)设置">
<!ENTITY zotero.__addonRef__.export.copy.enable.label "导出到剪贴板">
<!ENTITY zotero.__addonRef__.export.pdf.label "PDF设置">
<!ENTITY zotero.__addonRef__.export.pdf.enable.label "导出到PDF">
<!ENTITY zotero.__addonRef__.sync.title "同步状态">
<!ENTITY zotero.__addonRef__.sync.export.label "导出为...">
<!ENTITY zotero.__addonRef__.sync.path.label "同步到:">
<!ENTITY zotero.__addonRef__.sync.lastsync.label "最近同步:">
<!ENTITY zotero.__addonRef__.sync.enable.label "保持同步">
<!ENTITY zotero.__addonRef__.sync.dosync.label "立刻同步">
<!ENTITY zotero.__addonRef__.wizard.title "欢迎使用 Zotero Better Notes">
<!ENTITY zotero.__addonRef__.wizard.page1.header "Zotero Better Notes 用户指引">
<!ENTITY zotero.__addonRef__.wizard.page1.description "单击下一步以继续。">

View File

@ -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);

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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,

View File

@ -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,

214
src/sync.ts Normal file
View File

@ -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<Number[]> {
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;

3
typing/addon.d.ts vendored
View File

@ -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");
}

2
typing/global.d.ts vendored
View File

@ -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,