259 lines
7.5 KiB
TypeScript
259 lines
7.5 KiB
TypeScript
import { showHint } from "../../utils/hint";
|
|
import { getString } from "../../utils/locale";
|
|
import { getPref } from "../../utils/prefs";
|
|
import { jointPath } from "../../utils/str";
|
|
|
|
export { setSyncing, callSyncing };
|
|
|
|
function setSyncing() {
|
|
const syncPeriod = getPref("syncPeriodSeconds") as number;
|
|
const enableHint = addon.data.env === "development";
|
|
if (syncPeriod > 0) {
|
|
enableHint && showHint(`${getString("sync.start.hint")} ${syncPeriod} s`);
|
|
const timer = ztoolkit.getGlobal("setInterval")(
|
|
() => {
|
|
if (!addon.data.alive) {
|
|
showHint(getString("sync.stop.hint"));
|
|
ztoolkit.getGlobal("clearInterval")(timer);
|
|
}
|
|
// Only when Zotero is active and focused
|
|
if (
|
|
Zotero.getMainWindow().document.hasFocus() &&
|
|
(getPref("syncPeriodSeconds") as number) > 0
|
|
) {
|
|
callSyncing(undefined, {
|
|
quiet: true,
|
|
skipActive: true,
|
|
reason: "auto",
|
|
});
|
|
}
|
|
},
|
|
Number(syncPeriod) * 1000,
|
|
);
|
|
}
|
|
}
|
|
|
|
async function callSyncing(
|
|
items: Zotero.Item[] = [],
|
|
{ quiet, skipActive, reason } = {
|
|
quiet: true,
|
|
skipActive: true,
|
|
reason: "unknown",
|
|
},
|
|
) {
|
|
// Always log in development mode
|
|
if (addon.data.env === "development") {
|
|
quiet = false;
|
|
}
|
|
if (addon.data.sync.lock) {
|
|
// Only allow one task
|
|
return;
|
|
}
|
|
let progress;
|
|
// Wrap the code in try...catch so that the lock can be released anyway
|
|
try {
|
|
addon.data.sync.lock = true;
|
|
let skippedCount = 0;
|
|
if (!items || !items.length) {
|
|
items = Zotero.Items.get(await addon.api.sync.getSyncNoteIds());
|
|
} else {
|
|
items = items.filter((item) => addon.api.sync.isSyncNote(item.id));
|
|
}
|
|
if (items.length === 0) {
|
|
addon.data.sync.lock = false;
|
|
return;
|
|
}
|
|
if (skipActive) {
|
|
// Skip active note editors' targets
|
|
const activeNoteIds = Zotero.Notes._editorInstances
|
|
.filter(
|
|
(editor) =>
|
|
!Components.utils.isDeadWrapper(editor._iframeWindow) &&
|
|
editor._iframeWindow.document.hasFocus(),
|
|
)
|
|
.map((editor) => editor._item.id);
|
|
const filteredItems = items.filter(
|
|
(item) => !activeNoteIds.includes(item.id),
|
|
);
|
|
skippedCount = items.length - filteredItems.length;
|
|
items = filteredItems;
|
|
}
|
|
ztoolkit.log("sync start", reason, items.length, skippedCount);
|
|
|
|
if (!quiet) {
|
|
progress = new ztoolkit.ProgressWindow(
|
|
`[${getString("sync.running.hint.title")}] ${
|
|
addon.data.env === "development" ? reason : "Better Notes"
|
|
}`,
|
|
)
|
|
.createLine({
|
|
text: `[${getString("sync.running.hint.check")}] 0/${
|
|
items.length
|
|
} ...`,
|
|
type: "default",
|
|
progress: 1,
|
|
})
|
|
.show(-1);
|
|
}
|
|
// Export items of same dir in batch
|
|
const toExport = {} as Record<string, number[]>;
|
|
const toImport: SyncStatus[] = [];
|
|
const toDiff: SyncStatus[] = [];
|
|
const mdStatusMap = {} as Record<number, MDStatus>;
|
|
let i = 1;
|
|
for (const item of items) {
|
|
const syncStatus = addon.api.sync.getSyncStatus(item.id);
|
|
const filepath = syncStatus.path;
|
|
const mdStatus = await addon.api.sync.getMDStatus(item.id);
|
|
mdStatusMap[item.id] = mdStatus;
|
|
|
|
const compareResult = await doCompare(item, mdStatus);
|
|
switch (compareResult) {
|
|
case SyncCode.NoteAhead:
|
|
if (Object.keys(toExport).includes(filepath)) {
|
|
toExport[filepath].push(item.id);
|
|
} else {
|
|
toExport[filepath] = [item.id];
|
|
}
|
|
break;
|
|
case SyncCode.MDAhead:
|
|
toImport.push(syncStatus);
|
|
break;
|
|
case SyncCode.NeedDiff:
|
|
toDiff.push(syncStatus);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
progress?.changeLine({
|
|
text: `[${getString("sync.running.hint.check")}] ${i}/${
|
|
items.length
|
|
} ...`,
|
|
progress: ((i - 1) / items.length) * 100,
|
|
});
|
|
i += 1;
|
|
}
|
|
|
|
let totalCount = Object.keys(toExport).length;
|
|
ztoolkit.log("will be synced:", totalCount, toImport.length, toDiff.length);
|
|
|
|
i = 1;
|
|
for (const filepath of Object.keys(toExport)) {
|
|
progress?.changeLine({
|
|
text: `[${getString("sync.running.hint.updateMD")}] ${i}/${
|
|
items.length
|
|
} ...`,
|
|
progress: ((i - 1) / items.length) * 100,
|
|
});
|
|
const itemIDs = toExport[filepath];
|
|
await addon.api.$export.syncMDBatch(
|
|
filepath,
|
|
itemIDs,
|
|
itemIDs.map((id) => mdStatusMap[id].meta!),
|
|
);
|
|
i += 1;
|
|
}
|
|
i = 1;
|
|
totalCount = toImport.length;
|
|
for (const syncStatus of toImport) {
|
|
progress?.changeLine({
|
|
text: `[${getString(
|
|
"sync.running.hint.updateNote",
|
|
)}] ${i}/${totalCount}, ${toDiff.length} queuing...`,
|
|
progress: ((i - 1) / totalCount) * 100,
|
|
});
|
|
const item = Zotero.Items.get(syncStatus.itemID);
|
|
const filepath = jointPath(syncStatus.path, syncStatus.filename);
|
|
await addon.api.$import.fromMD(filepath, { noteId: item.id });
|
|
// Update md file to keep the metadata synced
|
|
await addon.api.$export.syncMDBatch(
|
|
syncStatus.path,
|
|
[item.id],
|
|
[mdStatusMap[item.id].meta!],
|
|
);
|
|
i += 1;
|
|
}
|
|
i = 1;
|
|
totalCount = toDiff.length;
|
|
for (const syncStatus of toDiff) {
|
|
progress?.changeLine({
|
|
text: `[${getString("sync.running.hint.diff")}] ${i}/${totalCount}...`,
|
|
progress: ((i - 1) / totalCount) * 100,
|
|
});
|
|
|
|
await addon.hooks.onShowSyncDiff(
|
|
syncStatus.itemID,
|
|
jointPath(syncStatus.path, syncStatus.filename),
|
|
);
|
|
i += 1;
|
|
}
|
|
const syncCount =
|
|
Object.keys(toExport).length + toImport.length + toDiff.length;
|
|
progress?.changeLine({
|
|
text:
|
|
(syncCount
|
|
? `[${getString(
|
|
"sync.running.hint.finish",
|
|
)}] ${syncCount} ${getString("sync.running.hint.synced")}`
|
|
: `[${getString("sync.running.hint.finish")}] ${getString(
|
|
"sync.running.hint.upToDate",
|
|
)}`) + (skippedCount ? `, ${skippedCount} skipped.` : ""),
|
|
progress: 100,
|
|
});
|
|
} catch (e) {
|
|
ztoolkit.log("[BetterNotes Syncing Error]", e);
|
|
showHint(`Sync Error: ${String(e)}`);
|
|
} finally {
|
|
progress?.startCloseTimer(5000);
|
|
}
|
|
addon.data.sync.lock = false;
|
|
}
|
|
|
|
async function doCompare(
|
|
noteItem: Zotero.Item,
|
|
mdStatus: MDStatus,
|
|
): Promise<SyncCode> {
|
|
const syncStatus = addon.api.sync.getSyncStatus(noteItem.id);
|
|
// No file found
|
|
if (!mdStatus.meta) {
|
|
return SyncCode.NoteAhead;
|
|
}
|
|
// File meta is unavailable
|
|
if (mdStatus.meta.$version < 0) {
|
|
return SyncCode.NeedDiff;
|
|
}
|
|
let MDAhead = false;
|
|
let noteAhead = false;
|
|
const md5 = Zotero.Utilities.Internal.md5(mdStatus.content, false);
|
|
const noteMd5 = Zotero.Utilities.Internal.md5(noteItem.getNote(), false);
|
|
// MD5 doesn't match (md side change)
|
|
if (md5 !== syncStatus.md5) {
|
|
MDAhead = true;
|
|
}
|
|
// MD5 doesn't match (note side change)
|
|
if (noteMd5 !== syncStatus.noteMd5) {
|
|
noteAhead = true;
|
|
}
|
|
// Note version doesn't match (note side change)
|
|
// This might be unreliable when Zotero account is not login
|
|
if (Number(mdStatus.meta.$version) !== noteItem.version) {
|
|
noteAhead = true;
|
|
}
|
|
if (noteAhead && MDAhead) {
|
|
return SyncCode.NeedDiff;
|
|
} else if (noteAhead) {
|
|
return SyncCode.NoteAhead;
|
|
} else if (MDAhead) {
|
|
return SyncCode.MDAhead;
|
|
} else {
|
|
return SyncCode.UpToDate;
|
|
}
|
|
}
|
|
|
|
enum SyncCode {
|
|
UpToDate = 0,
|
|
NoteAhead,
|
|
MDAhead,
|
|
NeedDiff,
|
|
}
|