New feature:

- `Sync on Editor save` has been implemented
- Now we can use the `Hidden file sync` and the `Customization sync` cooperatively.
- We can ignore specific plugins in Customization sync.
- Now the message of leftover conflicted files accepts our click.

Refactored:
- Parallelism functions made more explicit.
- Type errors have been reduced.

Fixed:
- Now documents would not be overwritten if they are conflicted.
- Some error messages have been fixed.
- Missing dialogue titles have been shown now.
This commit is contained in:
vorotamoroz
2023-09-19 09:53:48 +01:00
parent 5acbbe479e
commit bcce277c36
18 changed files with 571 additions and 241 deletions

View File

@@ -1,7 +1,7 @@
const isDebug = false;
import { type Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "./deps";
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, type RequestUrlParam, type RequestUrlResponse, requestUrl } from "./deps";
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, type RequestUrlParam, type RequestUrlResponse, requestUrl, type MarkdownFileInfo } from "./deps";
import { type EntryDoc, type LoadedEntry, type ObsidianLiveSyncSettings, type diff_check_result, type diff_result_leaf, type EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, type diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, SALT_OF_PASSPHRASE, type ConfigPassphraseStore, type CouchDBConnection, FLAGMD_REDFLAG2, FLAGMD_REDFLAG3, PREFIXMD_LOGFILE, type DatabaseConnectingStatus, type EntryHasPath, type DocumentID, type FilePathWithPrefix, type FilePath, type AnyEntry, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_URGENT, LOG_LEVEL_VERBOSE } from "./lib/src/types";
import { type InternalFileInfo, type queueItem, type CacheData, type FileEventItem, FileWatchEventQueueMax } from "./types";
import { arrayToChunkedArray, getDocData, isDocContentSame } from "./lib/src/utils";
@@ -10,15 +10,15 @@ import { PouchDB } from "./lib/src/pouchdb-browser.js";
import { ConflictResolveModal } from "./ConflictResolveModal";
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
import { DocumentHistoryModal } from "./DocumentHistoryModal";
import { applyPatch, cancelAllPeriodicTask, cancelAllTasks, cancelTask, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, flattenObject, path2id, scheduleTask, tryParseJSON, createFile, modifyFile, isValidPath, getAbstractFileByPath, touch, recentlyTouched, isInternalMetadata, isPluginMetadata, stripInternalMetadataPrefix, isChunk, askSelectString, askYesNo, askString, PeriodicProcessor, clearTouched, getPath, getPathWithoutPrefix, getPathFromTFile, performRebuildDB } from "./utils";
import { applyPatch, cancelAllPeriodicTask, cancelAllTasks, cancelTask, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, flattenObject, path2id, scheduleTask, tryParseJSON, createFile, modifyFile, isValidPath, getAbstractFileByPath, touch, recentlyTouched, isInternalMetadata, isPluginMetadata, stripInternalMetadataPrefix, isChunk, askSelectString, askYesNo, askString, PeriodicProcessor, clearTouched, getPath, getPathWithoutPrefix, getPathFromTFile, performRebuildDB, memoIfNotExist, memoObject, retrieveMemoObject, disposeMemoObject, isCustomisationSyncMetadata } from "./utils";
import { encrypt, tryDecrypt } from "./lib/src/e2ee_v2";
import { balanceChunkPurgedDBs, enableEncryption, isCloudantURI, isErrorOfMissingDoc, isValidRemoteCouchDBURI, purgeUnreferencedChunks } from "./lib/src/utils_couchdb";
import { getGlobalStore, ObservableStore, observeStores } from "./lib/src/store";
import { lockStore, logMessageStore, logStore, type LogEntry } from "./lib/src/stores";
import { setNoticeClass } from "./lib/src/wrapper";
import { base64ToString, versionNumberString2Number, base64ToArrayBuffer, arrayBufferToBase64 } from "./lib/src/strbin";
import { base64ToString, versionNumberString2Number, base64ToArrayBuffer, arrayBufferToBase64, writeString } from "./lib/src/strbin";
import { addPrefix, isAcceptedAll, isPlainText, shouldBeIgnored, stripAllPrefixes } from "./lib/src/path";
import { isLockAcquired, runWithLock } from "./lib/src/lock";
import { isLockAcquired, serialized, skipIfDuplicated } from "./lib/src/lock";
import { Semaphore } from "./lib/src/semaphore";
import { StorageEventManager, StorageEventManagerObsidian } from "./StorageEventManager";
import { LiveSyncLocalDB, type LiveSyncLocalDBEnv } from "./lib/src/LiveSyncLocalDB";
@@ -43,16 +43,29 @@ setGlobalLogFunction((message: any, level?: LOG_LEVEL, key?: string) => {
});
logStore.intercept(e => e.slice(Math.min(e.length - 200, 0)));
async function fetchByAPI(request: RequestUrlParam): Promise<RequestUrlResponse> {
const ret = await requestUrl(request);
if (ret.status - (ret.status % 100) !== 200) {
const er: Error & { status?: number } = new Error(`Request Error:${ret.status}`);
if (ret.json) {
er.message = ret.json.reason;
er.name = `${ret.json.error ?? ""}:${ret.json.message ?? ""}`;
}
er.status = ret.status;
throw er;
}
return ret;
}
export default class ObsidianLiveSyncPlugin extends Plugin
implements LiveSyncLocalDBEnv, LiveSyncReplicatorEnv {
settings: ObsidianLiveSyncSettings;
localDatabase: LiveSyncLocalDB;
replicator: LiveSyncDBReplicator;
settings!: ObsidianLiveSyncSettings;
localDatabase!: LiveSyncLocalDB;
replicator!: LiveSyncDBReplicator;
statusBar: HTMLElement;
suspended: boolean;
deviceAndVaultName: string;
statusBar?: HTMLElement;
suspended: boolean = true;
deviceAndVaultName: string = "";
isMobile = false;
isReady = false;
packageVersion = "";
@@ -67,25 +80,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin
periodicSyncProcessor = new PeriodicProcessor(this, async () => await this.replicate());
// implementing interfaces
kvDB: KeyValueDatabase;
kvDB!: KeyValueDatabase;
last_successful_post = false;
getLastPostFailedBySize() {
return !this.last_successful_post;
}
async fetchByAPI(request: RequestUrlParam): Promise<RequestUrlResponse> {
const ret = await requestUrl(request);
if (ret.status - (ret.status % 100) !== 200) {
const er: Error & { status?: number } = new Error(`Request Error:${ret.status}`);
if (ret.json) {
er.message = ret.json.reason;
er.name = `${ret.json.error ?? ""}:${ret.json.message ?? ""}`;
}
er.status = ret.status;
throw er;
}
return ret;
}
_unloaded = false;
getDatabase(): PouchDB.Database<EntryDoc> {
return this.localDatabase.localDatabase;
}
@@ -103,7 +106,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
if (uri.indexOf(" ") !== -1) return "Remote URI and database name could not contain spaces.";
let authHeader = "";
if (auth.username && auth.password) {
const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${auth.username}:${auth.password}`));
const utf8str = String.fromCharCode.apply(null, [...writeString(`${auth.username}:${auth.password}`)]);
const encoded = window.btoa(utf8str);
authHeader = "Basic " + encoded;
} else {
@@ -115,11 +118,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin
adapter: "http",
auth,
skip_setup: !performSetup,
fetch: async (url: string | Request, opts: RequestInit) => {
fetch: async (url: string | Request, opts?: RequestInit) => {
let size = "";
const localURL = url.toString().substring(uri.length);
const method = opts.method ?? "GET";
if (opts.body) {
const method = opts?.method ?? "GET";
if (opts?.body) {
const opts_length = opts.body.toString().length;
if (opts_length > 1000 * 1000 * 10) {
// over 10MB
@@ -132,10 +135,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin
size = ` (${opts_length})`;
}
if (!disableRequestURI && typeof url == "string" && typeof (opts.body ?? "") == "string") {
const body = opts.body as string;
if (!disableRequestURI && typeof url == "string" && typeof (opts?.body ?? "") == "string") {
const body = opts?.body as string;
const transformedHeaders = { ...(opts.headers as Record<string, string>) };
const transformedHeaders = { ...(opts?.headers as Record<string, string>) };
if (authHeader != "") transformedHeaders["authorization"] = authHeader;
delete transformedHeaders["host"];
delete transformedHeaders["Host"];
@@ -143,7 +146,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
delete transformedHeaders["Content-Length"];
const requestParam: RequestUrlParam = {
url,
method: opts.method,
method: opts?.method,
body: body,
headers: transformedHeaders,
contentType: "application/json",
@@ -151,7 +154,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
};
try {
const r = await this.fetchByAPI(requestParam);
const r = await fetchByAPI(requestParam);
if (method == "POST" || method == "PUT") {
this.last_successful_post = r.status - (r.status % 100) == 200;
} else {
@@ -209,9 +212,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin
try {
const info = await db.info();
return { db: db, info: info };
} catch (ex) {
let msg = `${ex.name}:${ex.message}`;
if (ex.name == "TypeError" && ex.message == "Failed to fetch") {
} catch (ex: any) {
let msg = `${ex?.name}:${ex?.message}`;
if (ex?.name == "TypeError" && ex?.message == "Failed to fetch") {
msg += "\n**Note** This error caused by many reasons. The only sure thing is you didn't touch the server.\nTo check details, open inspector.";
}
Logger(ex, LOG_LEVEL_VERBOSE);
@@ -219,7 +222,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
}
}
id2path(id: DocumentID, entry: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix {
id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix {
const tempId = id2path(id, entry);
if (stripPrefix && isInternalMetadata(tempId)) {
const out = stripInternalMetadataPrefix(tempId);
@@ -234,18 +237,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin
return getPathWithoutPrefix(entry);
}
async path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise<DocumentID> {
const destPath = addPrefix(filename, prefix);
const destPath = addPrefix(filename, prefix ?? "");
return await path2id(destPath, this.settings.usePathObfuscation ? this.settings.passphrase : "");
}
createPouchDBInstance<T>(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database<T> {
createPouchDBInstance<T extends object>(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database<T> {
const optionPass = options ?? {};
if (this.settings.useIndexedDBAdapter) {
options.adapter = "indexeddb";
optionPass.adapter = "indexeddb";
//@ts-ignore :missing def
options.purged_infos_limit = 1;
return new PouchDB(name + "-indexeddb", options);
optionPass.purged_infos_limit = 1;
return new PouchDB(name + "-indexeddb", optionPass);
}
return new PouchDB(name, options);
return new PouchDB(name, optionPass);
}
beforeOnUnload(db: LiveSyncLocalDB): void {
this.kvDB.close();
@@ -258,6 +262,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
this.replicator = new LiveSyncDBReplicator(this);
}
async onResetDatabase(db: LiveSyncLocalDB): Promise<void> {
const lsKey = "obsidian-livesync-queuefiles-" + this.getVaultName();
localStorage.removeItem(lsKey);
await this.kvDB.destroy();
this.kvDB = await OpenKeyValueDatabase(db.dbname + "-livesync-kv");
this.replicator = new LiveSyncDBReplicator(this);
@@ -320,7 +326,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
}
}
showHistory(file: TFile | FilePathWithPrefix, id: DocumentID) {
showHistory(file: TFile | FilePathWithPrefix, id?: DocumentID) {
new DocumentHistoryModal(this.app, this, file, id).open();
}
@@ -334,7 +340,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
const target = await askSelectString(this.app, "File to view History", notesList);
if (target) {
const targetId = notes.find(e => e.dispPath == target);
this.showHistory(targetId.path, undefined);
this.showHistory(targetId.path, targetId.id);
}
}
async pickFileForResolve() {
@@ -349,7 +355,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
Logger("There are no conflicted documents", LOG_LEVEL_NOTICE);
return false;
}
const target = await askSelectString(this.app, "File to view History", notesList);
const target = await askSelectString(this.app, "File to resolve conflict", notesList);
if (target) {
const targetItem = notes.find(e => e.dispPath == target);
await this.resolveConflicted(targetItem.path);
@@ -363,6 +369,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
await this.addOnHiddenFileSync.resolveConflictOnInternalFile(target);
} else if (isPluginMetadata(target)) {
await this.resolveConflictByNewerEntry(target);
} else if (isCustomisationSyncMetadata(target)) {
await this.resolveConflictByNewerEntry(target);
} else {
await this.showIfConflicted(target);
}
@@ -453,6 +461,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
}
await this.realizeSettingSyncMode();
this.registerWatchEvents();
this.swapSaveCommand();
if (this.settings.syncOnStart) {
this.replicator.openReplication(this.settings, false, false);
}
@@ -474,7 +483,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin
notes.push({ path: this.getPath(doc), mtime: doc.mtime });
}
if (notes.length > 0) {
Logger(`Some files have been left conflicted! Please resolve them by "Pick a file to resolve conflict". The list is written in the log.`, LOG_LEVEL_NOTICE);
this.askInPopup(`conflicting-detected-on-safety`, `Some files have been left conflicted! Press {HERE} to resolve them, or you can do it later by "Pick a file to resolve conflict`, (anchor) => {
anchor.text = "HERE";
anchor.addEventListener("click", () => {
// @ts-ignore
this.app.commands.executeCommandById("obsidian-livesync:livesync-all-conflictcheck");
});
}
);
Logger(`Some files have been left conflicted! Please resolve them by "Pick a file to resolve conflict". The list is written in the log.`, LOG_LEVEL_VERBOSE);
for (const note of notes) {
Logger(`Conflicted: ${note.path}`);
}
@@ -512,6 +529,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
if (last_version && Number(last_version) < VER) {
this.settings.liveSync = false;
this.settings.syncOnSave = false;
this.settings.syncOnEditorSave = false;
this.settings.syncOnStart = false;
this.settings.syncOnFileOpen = false;
this.settings.syncAfterMerge = false;
@@ -572,15 +590,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin
this.addCommand({
id: "livesync-dump",
name: "Dump information of this doc ",
editorCallback: (editor: Editor, view: MarkdownView) => {
this.localDatabase.getDBEntry(getPathFromTFile(view.file), {}, true, false);
editorCallback: (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
const file = view.file;
if (!file) return;
this.localDatabase.getDBEntry(getPathFromTFile(file), {}, true, false);
},
});
this.addCommand({
id: "livesync-checkdoc-conflicted",
name: "Resolve if conflicted.",
editorCallback: async (editor: Editor, view: MarkdownView) => {
await this.showIfConflicted(getPathFromTFile(view.file));
editorCallback: async (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
const file = view.file;
if (!file) return;
await this.showIfConflicted(getPathFromTFile(file));
},
});
@@ -617,8 +639,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
this.addCommand({
id: "livesync-history",
name: "Show history",
editorCallback: (editor: Editor, view: MarkdownView) => {
this.showHistory(view.file, null);
editorCallback: (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
if (view.file) this.showHistory(view.file, null);
},
});
this.addCommand({
@@ -720,6 +742,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
}
cancelAllPeriodicTask();
cancelAllTasks();
this._unloaded = true;
Logger("unloading plugin");
}
@@ -852,7 +875,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
(async () => await this.realizeSettingSyncMode())();
}
async saveSettings() {
async saveSettingData() {
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.getVaultName();
localStorage.setItem(lsKey, this.deviceAndVaultName || "");
@@ -882,6 +906,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin
await this.saveData(settings);
this.localDatabase.settings = this.settings;
this.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim());
}
async saveSettings() {
await this.saveSettingData();
this.triggerRealizeSettingSyncMode();
}
@@ -889,7 +917,38 @@ export default class ObsidianLiveSyncPlugin extends Plugin
registerFileWatchEvents() {
this.vaultManager = new StorageEventManagerObsidian(this)
}
_initialCallback: any;
swapSaveCommand() {
Logger("Modifying callback of the save command", LOG_LEVEL_VERBOSE);
const saveCommandDefinition = (this.app as any).commands?.commands?.[
"editor:save-file"
];
const save = saveCommandDefinition?.callback;
if (typeof save === "function") {
this._initialCallback = save;
saveCommandDefinition.callback = () => {
scheduleTask("syncOnEditorSave", 250, () => {
if (this._unloaded) {
Logger("Unload and remove the handler.", LOG_LEVEL_VERBOSE);
saveCommandDefinition.callback = this._initialCallback;
} else {
Logger("Sync on Editor Save.", LOG_LEVEL_VERBOSE);
if (this.settings.syncOnEditorSave) {
this.replicate();
}
}
});
save();
};
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const _this = this;
//@ts-ignore
window.CodeMirrorAdapter.commands.save = () => {
//@ts-ignore
_this.app.commands.executeCommandById('editor:save-file');
};
}
registerWatchEvents() {
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
this.registerDomEvent(document, "visibilitychange", this.watchWindowVisibility);
@@ -949,7 +1008,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
}
}
cancelTask("applyBatchAuto");
const ret = await runWithLock("procFiles", true, async () => {
const ret = await skipIfDuplicated("procFiles", async () => {
do {
const queue = this.vaultManager.fetchEvent();
if (queue === false) break;
@@ -1004,9 +1063,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin
}
watchWorkspaceOpen(file: TFile) {
watchWorkspaceOpen(file: TFile | null) {
if (this.settings.suspendFileWatching) return;
if (!this.isReady) return;
if (!file) return;
this.watchWorkspaceOpenAsync(file);
}
@@ -1269,7 +1329,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
const path = getPath(entry);
try {
const releaser = await semaphore.acquire(1);
runWithLock(`dbchanged-${path}`, false, async () => {
serialized(`dbchanged-${path}`, async () => {
Logger(`Applying ${path} (${entry._id}: ${entry._rev}) change...`, LOG_LEVEL_VERBOSE);
await this.handleDBChangedAsync(entry);
Logger(`Applied ${path} (${entry._id}:${entry._rev}) change...`);
@@ -1646,7 +1706,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
if (this.replicator.remoteLockedAndDeviceNotAccepted) {
if (this.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) {
Logger(`The remote database has been cleaned.`, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
await runWithLock("cleanup", true, async () => {
await skipIfDuplicated("cleanup", async () => {
const count = await purgeUnreferencedChunks(this.localDatabase.localDatabase, true);
const message = `The remote database has been cleaned up.
To synchronize, this device must be also cleaned up. ${count} chunk(s) will be erased from this device.
@@ -2172,18 +2232,11 @@ Or if you are sure know what had been happened, we can unlock the database from
Logger(`could not get old revisions, automatically used newer one:${path}`, LOG_LEVEL_NOTICE);
return true;
}
// first, check for same contents and deletion status.
if (leftLeaf.data == rightLeaf.data && leftLeaf.deleted == rightLeaf.deleted) {
let leaf = leftLeaf;
if (leftLeaf.mtime > rightLeaf.mtime) {
leaf = rightLeaf;
}
await this.localDatabase.deleteDBEntry(path, { rev: leaf.rev });
await this.pullFile(path, null, true);
Logger(`automatically merged:${path}`);
return true;
}
if (this.settings.resolveConflictsByNewerFile) {
const isSame = leftLeaf.data == rightLeaf.data && leftLeaf.deleted == rightLeaf.deleted;
const isBinary = !isPlainText(path);
const alwaysNewer = this.settings.resolveConflictsByNewerFile;
if (isSame || isBinary || alwaysNewer) {
const lMtime = ~~(leftLeaf.mtime / 1000);
const rMtime = ~~(rightLeaf.mtime / 1000);
let loser = leftLeaf;
@@ -2192,7 +2245,7 @@ Or if you are sure know what had been happened, we can unlock the database from
}
await this.localDatabase.deleteDBEntry(path, { rev: loser.rev });
await this.pullFile(path, null, true);
Logger(`Automatically merged (newerFileResolve) :${path}`, LOG_LEVEL_NOTICE);
Logger(`Automatically merged (${isSame ? "same," : ""}${isBinary ? "binary," : ""}${alwaysNewer ? "alwaysNewer" : ""}) :${path}`, LOG_LEVEL_NOTICE);
return true;
}
// make diff.
@@ -2208,7 +2261,7 @@ Or if you are sure know what had been happened, we can unlock the database from
}
showMergeDialog(filename: FilePathWithPrefix, conflictCheckResult: diff_result): Promise<boolean> {
return runWithLock("resolve-conflict:" + filename, false, () =>
return serialized("resolve-conflict:" + filename, () =>
new Promise((res, rej) => {
Logger("open conflict dialog", LOG_LEVEL_VERBOSE);
new ConflictResolveModal(this.app, filename, conflictCheckResult, async (selected) => {
@@ -2287,7 +2340,7 @@ Or if you are sure know what had been happened, we can unlock the database from
}
async showIfConflicted(filename: FilePathWithPrefix) {
await runWithLock("conflicted", false, async () => {
await serialized("conflicted", async () => {
const conflictCheckResult = await this.getConflictedStatus(filename);
if (conflictCheckResult === false) {
//nothing to do.
@@ -2439,7 +2492,7 @@ Or if you are sure know what had been happened, we can unlock the database from
};
//upsert should locked
const msg = `DB <- STORAGE (${datatype}) `;
const isNotChanged = await runWithLock("file-" + fullPath, false, async () => {
const isNotChanged = await serialized("file-" + fullPath, async () => {
if (recentlyTouched(file)) {
return true;
}
@@ -2608,7 +2661,7 @@ Or if you are sure know what had been happened, we can unlock the database from
return this.localDatabase.isTargetFile(filepath);
}
async dryRunGC() {
await runWithLock("cleanup", true, async () => {
await skipIfDuplicated("cleanup", async () => {
const remoteDBConn = await this.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.isMobile)
if (typeof (remoteDBConn) == "string") {
Logger(remoteDBConn);
@@ -2622,7 +2675,7 @@ Or if you are sure know what had been happened, we can unlock the database from
async dbGC() {
// Lock the remote completely once.
await runWithLock("cleanup", true, async () => {
await skipIfDuplicated("cleanup", async () => {
this.getReplicator().markRemoteLocked(this.settings, true, true);
const remoteDBConn = await this.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.isMobile)
if (typeof (remoteDBConn) == "string") {
@@ -2637,5 +2690,41 @@ Or if you are sure know what had been happened, we can unlock the database from
Logger("The remote database has been cleaned up! Other devices will be cleaned up on the next synchronisation.")
});
}
askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void) {
const fragment = createFragment((doc) => {
const [beforeText, afterText] = dialogText.split("{HERE}", 2);
doc.createEl("span", null, (a) => {
a.appendText(beforeText);
a.appendChild(a.createEl("a", null, (anchor) => {
anchorCallback(anchor);
}));
a.appendText(afterText);
});
});
const popupKey = "popup-" + key;
scheduleTask(popupKey, 1000, async () => {
const popup = await memoIfNotExist(popupKey, () => new Notice(fragment, 0));
//@ts-ignore
const isShown = popup?.noticeEl?.isShown();
if (!isShown) {
memoObject(popupKey, new Notice(fragment, 0));
}
scheduleTask(popupKey + "-close", 20000, () => {
const popup = retrieveMemoObject<Notice>(popupKey);
if (!popup)
return;
//@ts-ignore
if (popup?.noticeEl?.isShown()) {
popup.hide();
}
disposeMemoObject(popupKey);
});
});
}
}