diff --git a/src/CmdConfigSync.ts b/src/CmdConfigSync.ts index 3b309c9..7e2af1f 100644 --- a/src/CmdConfigSync.ts +++ b/src/CmdConfigSync.ts @@ -1,14 +1,14 @@ import { writable } from 'svelte/store'; -import { Notice, type PluginManifest, parseYaml } from "./deps"; +import { Notice, type PluginManifest, parseYaml, normalizePath } from "./deps"; import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, DocumentID, AnyEntry } from "./lib/src/types"; -import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "./lib/src/types"; +import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE } from "./lib/src/types"; import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "./types"; import { delay, getDocData } from "./lib/src/utils"; import { Logger } from "./lib/src/logger"; import { WrappedNotice } from "./lib/src/wrapper"; import { base64ToArrayBuffer, arrayBufferToBase64, readString, crc32CKHash } from "./lib/src/strbin"; -import { runWithLock } from "./lib/src/lock"; +import { serialized } from "./lib/src/lock"; import { LiveSyncCommands } from "./LiveSyncCommands"; import { stripAllPrefixes } from "./lib/src/path"; import { PeriodicProcessor, askYesNo, disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "./utils"; @@ -16,10 +16,20 @@ import { PluginDialogModal } from "./dialogs"; import { JsonResolveModal } from "./JsonResolveModal"; import { pipeGeneratorToGenerator, processAllGeneratorTasksWithConcurrencyLimit } from './lib/src/task'; - -function serialize(obj: T): string { - return JSON.stringify(obj, null, 1); +function serialize(data: PluginDataEx): string { + // To improve performance, make JSON manually. + // Self-hosted LiveSync uses `\n` to split chunks. Therefore, grouping together those with similar entropy would work nicely. + return `{"category":"${data.category}","name":"${data.name}","term":${JSON.stringify(data.term)} +${data.version ? `,"version":"${data.version}"` : ""}, +"mtime":${data.mtime}, +"files":[ +${data.files.map(file => `{"filename":"${file.filename}"${file.displayName ? `,"displayName":"${file.displayName}"` : ""}${file.version ? `,"version":"${file.version}"` : ""}, +"mtime":${file.mtime},"size":${file.size} +,"data":[${file.data.map(e => `"${e}"`).join(",") + }]}`).join(",") + }]}` } + function deserialize(str: string, def: T) { try { return JSON.parse(str) as T; @@ -107,6 +117,7 @@ export class ConfigSync extends LiveSyncCommands { }, }); } + getFileCategory(filePath: string): "CONFIG" | "THEME" | "SNIPPET" | "PLUGIN_MAIN" | "PLUGIN_ETC" | "PLUGIN_DATA" | "" { if (filePath.split("/").length == 2 && filePath.endsWith(".json")) return "CONFIG"; if (filePath.split("/").length == 4 && filePath.startsWith(`${this.app.vault.configDir}/themes/`)) return "THEME"; @@ -164,6 +175,46 @@ export class ConfigSync extends LiveSyncCommands { pluginList.set(this.pluginList) await this.updatePluginList(showMessage); } + async loadPluginData(path: FilePathWithPrefix): Promise { + const wx = await this.localDatabase.getDBEntry(path, null, false, false); + if (wx) { + const data = deserialize(getDocData(wx.data), {}) as PluginDataEx; + const xFiles = [] as PluginDataExFile[]; + for (const file of data.files) { + const work = { ...file }; + const tempStr = getDocData(work.data); + work.data = [crc32CKHash(tempStr)]; + xFiles.push(work); + } + return ({ + ...data, + documentPath: this.getPath(wx), + files: xFiles + }) as PluginDataExDisplay; + } + return false; + } + createMissingConfigurationEntry() { + let saveRequired = false; + for (const v of this.pluginList) { + const key = `${v.category}/${v.name}`; + if (!(key in this.plugin.settings.pluginSyncExtendedSetting)) { + this.plugin.settings.pluginSyncExtendedSetting[key] = { + key, + mode: MODE_SELECTIVE, + files: [] + } + } + if (this.plugin.settings.pluginSyncExtendedSetting[key].files.sort().join(",").toLowerCase() != + v.files.map(e => e.filename).sort().join(",").toLowerCase()) { + this.plugin.settings.pluginSyncExtendedSetting[key].files = v.files.map(e => e.filename).sort(); + saveRequired = true; + } + } + if (saveRequired) { + this.plugin.saveSettingData(); + } + } async updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise { const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO; // pluginList.set([]); @@ -174,7 +225,7 @@ export class ConfigSync extends LiveSyncCommands { } await Promise.resolve(); // Just to prevent warning. scheduleTask("update-plugin-list-task", 200, async () => { - await runWithLock("update-plugin-list", false, async () => { + await serialized("update-plugin-list", async () => { try { const updatedDocumentId = updatedDocumentPath ? await this.path2id(updatedDocumentPath) : ""; const plugins = updatedDocumentPath ? @@ -193,22 +244,7 @@ export class ConfigSync extends LiveSyncCommands { count++; if (count % 10 == 0) Logger(`Enumerating files... ${count}`, logLevel, "get-plugins"); Logger(`plugin-${path}`, LOG_LEVEL_VERBOSE); - const wx = await this.localDatabase.getDBEntry(path, null, false, false); - if (wx) { - const data = deserialize(getDocData(wx.data), {}) as PluginDataEx; - const xFiles = [] as PluginDataExFile[]; - for (const file of data.files) { - const work = { ...file }; - const tempStr = getDocData(work.data); - work.data = [crc32CKHash(tempStr)]; - xFiles.push(work); - } - return ({ - ...data, - documentPath: this.getPath(wx), - files: xFiles - }); - } + return this.loadPluginData(path); // return entries; } catch (ex) { //TODO @@ -218,7 +254,7 @@ export class ConfigSync extends LiveSyncCommands { return false; }))) { if ("ok" in v) { - if (v.ok != false) { + if (v.ok !== false) { let newList = [...this.pluginList]; const item = v.ok; newList = newList.filter(x => x.documentPath != item.documentPath); @@ -230,6 +266,7 @@ export class ConfigSync extends LiveSyncCommands { } } Logger(`All files enumerated`, logLevel, "get-plugins"); + this.createMissingConfigurationEntry(); } finally { pluginIsEnumerating.set(false); } @@ -257,7 +294,7 @@ export class ConfigSync extends LiveSyncCommands { const fileA = { ...pluginDataA.files[0], ctime: pluginDataA.files[0].mtime, _id: `${pluginDataA.documentPath}` as DocumentID }; const fileB = pluginDataB.files[0]; const docAx = { ...docA, ...fileA } as LoadedEntry, docBx = { ...docB, ...fileB } as LoadedEntry - return runWithLock("config:merge-data", false, () => new Promise((res) => { + return serialized("config:merge-data", () => new Promise((res) => { Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE); // const docs = [docA, docB]; const path = stripAllPrefixes(docAx.path.split("/").slice(-1).join("/") as FilePath); @@ -411,6 +448,7 @@ export class ConfigSync extends LiveSyncCommands { this.periodicPluginSweepProcessor.enable(this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges ? (PERIODIC_PLUGIN_SWEEP * 1000) : 0); return; } + recentProcessedInternalFiles = [] as string[]; async makeEntryFromFile(path: FilePath): Promise { const stat = await this.app.vault.adapter.stat(path); @@ -470,7 +508,7 @@ export class ConfigSync extends LiveSyncCommands { return; } const vf = this.filenameToUnifiedKey(path, term); - return await runWithLock(`plugin-${vf}`, false, async () => { + return await serialized(`plugin-${vf}`, async () => { const category = this.getFileCategory(path); let mtime = 0; let fileTargets = [] as FilePath[]; @@ -578,6 +616,13 @@ export class ConfigSync extends LiveSyncCommands { // Make sure that target is a file. if (stat && stat.type != "file") return false; + + const configDir = normalizePath(this.app.vault.configDir); + const synchronisedInConfigSync = Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode != MODE_SELECTIVE).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase()); + if (synchronisedInConfigSync.some(e => e.startsWith(path.toLowerCase()))) { + Logger(`Customization file skipped: ${path}`, LOG_LEVEL_VERBOSE); + return; + } const storageMTime = ~~((stat && stat.mtime || 0) / 1000); const key = `${path}-${storageMTime}`; if (this.recentProcessedInternalFiles.contains(key)) { @@ -618,7 +663,7 @@ export class ConfigSync extends LiveSyncCommands { // const id = await this.path2id(prefixedFileName); const mtime = new Date().getTime(); - await runWithLock("file-x-" + prefixedFileName, false, async () => { + await serialized("file-x-" + prefixedFileName, async () => { try { const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, null, false) as InternalFileEntry | false; let saveData: InternalFileEntry; diff --git a/src/CmdHiddenFileSync.ts b/src/CmdHiddenFileSync.ts index b4dd6a6..87c14d7 100644 --- a/src/CmdHiddenFileSync.ts +++ b/src/CmdHiddenFileSync.ts @@ -1,13 +1,13 @@ -import { Notice, normalizePath, type PluginManifest } from "./deps"; -import { type EntryDoc, type LoadedEntry, type InternalFileEntry, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "./lib/src/types"; +import { normalizePath, type PluginManifest } from "./deps"; +import { type EntryDoc, type LoadedEntry, type InternalFileEntry, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_PAUSED } from "./lib/src/types"; import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "./types"; import { Parallels, delay, isDocContentSame } from "./lib/src/utils"; import { Logger } from "./lib/src/logger"; import { PouchDB } from "./lib/src/pouchdb-browser.js"; -import { disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask, isInternalMetadata, PeriodicProcessor } from "./utils"; +import { scheduleTask, isInternalMetadata, PeriodicProcessor } from "./utils"; import { WrappedNotice } from "./lib/src/wrapper"; import { base64ToArrayBuffer, arrayBufferToBase64 } from "./lib/src/strbin"; -import { runWithLock } from "./lib/src/lock"; +import { serialized } from "./lib/src/lock"; import { JsonResolveModal } from "./JsonResolveModal"; import { LiveSyncCommands } from "./LiveSyncCommands"; import { addPrefix, stripAllPrefixes } from "./lib/src/path"; @@ -77,7 +77,7 @@ export class HiddenFileSync extends LiveSyncCommands { procInternalFiles: string[] = []; async execInternalFile() { - await runWithLock("execInternal", false, async () => { + await serialized("execInternal", async () => { const w = [...this.procInternalFiles]; this.procInternalFiles = []; Logger(`Applying hidden ${w.length} files change...`); @@ -95,6 +95,14 @@ export class HiddenFileSync extends LiveSyncCommands { recentProcessedInternalFiles = [] as string[]; async watchVaultRawEventsAsync(path: FilePath) { if (!this.settings.syncInternalFiles) return; + + // Exclude files handled by customization sync + const configDir = normalizePath(this.app.vault.configDir); + const synchronisedInConfigSync = !this.settings.usePluginSync ? [] : Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode == MODE_SELECTIVE || e.mode == MODE_PAUSED).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase()); + if (synchronisedInConfigSync.some(e => e.startsWith(path.toLowerCase()))) { + Logger(`Hidden file skipped: ${path} is synchronized in customization sync.`, LOG_LEVEL_VERBOSE); + return; + } const stat = await this.app.vault.adapter.stat(path); // sometimes folder is coming. if (stat && stat.type != "file") @@ -209,18 +217,24 @@ export class HiddenFileSync extends LiveSyncCommands { } //TODO: Tidy up. Even though it is experimental feature, So dirty... - async syncInternalFilesAndDatabase(direction: "push" | "pull" | "safe" | "pullForce" | "pushForce", showMessage: boolean, files: InternalFileInfo[] | false = false, targetFiles: string[] | false = false) { + async syncInternalFilesAndDatabase(direction: "push" | "pull" | "safe" | "pullForce" | "pushForce", showMessage: boolean, filesAll: InternalFileInfo[] | false = false, targetFiles: string[] | false = false) { await this.resolveConflictOnInternalFiles(); const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO; Logger("Scanning hidden files.", logLevel, "sync_internal"); const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns .replace(/\n| /g, "") .split(",").filter(e => e).map(e => new RegExp(e, "i")); - if (!files) - files = await this.scanInternalFiles(); + + const configDir = normalizePath(this.app.vault.configDir); + let files: InternalFileInfo[] = + filesAll ? filesAll : (await this.scanInternalFiles()) + + const synchronisedInConfigSync = !this.settings.usePluginSync ? [] : Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode == MODE_SELECTIVE || e.mode == MODE_PAUSED).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase()); + files = files.filter(file => synchronisedInConfigSync.every(filterFile => !file.path.toLowerCase().startsWith(filterFile))) + const filesOnDB = ((await this.localDatabase.allDocsRaw({ startkey: ICHeader, endkey: ICHeaderEnd, include_docs: true })).rows.map(e => e.doc) as InternalFileEntry[]).filter(e => !e.deleted); const allFileNamesSrc = [...new Set([...files.map(e => normalizePath(e.path)), ...filesOnDB.map(e => stripAllPrefixes(this.getPath(e)))])]; - const allFileNames = allFileNamesSrc.filter(filename => !targetFiles || (targetFiles && targetFiles.indexOf(filename) !== -1)); + const allFileNames = allFileNamesSrc.filter(filename => !targetFiles || (targetFiles && targetFiles.indexOf(filename) !== -1)).filter(path => synchronisedInConfigSync.every(filterFile => !path.toLowerCase().startsWith(filterFile))) function compareMTime(a: number, b: number) { const wa = ~~(a / 1000); const wb = ~~(b / 1000); @@ -274,7 +288,7 @@ export class HiddenFileSync extends LiveSyncCommands { if (ignorePatterns.some(e => filename.match(e))) continue; if (await this.plugin.isIgnoredByIgnoreFiles(filename)) { - continue + continue; } const fileOnStorage = filename in filesMap ? filesMap[filename] : undefined; @@ -335,7 +349,6 @@ export class HiddenFileSync extends LiveSyncCommands { // When files has been retrieved from the database. they must be reloaded. if ((direction == "pull" || direction == "pullForce") && filesChanged != 0) { - const configDir = normalizePath(this.app.vault.configDir); // Show notification to restart obsidian when something has been changed in configDir. if (configDir in updatedFolders) { // Numbers of updated files that is below of configDir. @@ -352,44 +365,18 @@ export class HiddenFileSync extends LiveSyncCommands { updatedCount -= updatedFolders[manifest.dir]; const updatePluginId = manifest.id; const updatePluginName = manifest.name; - const fragment = createFragment((doc) => { - doc.createEl("span", null, (a) => { - a.appendText(`Files in ${updatePluginName} has been updated, Press `); - a.appendChild(a.createEl("a", null, (anchor) => { - anchor.text = "HERE"; - anchor.addEventListener("click", async () => { - Logger(`Unloading plugin: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId); - // @ts-ignore - await this.app.plugins.unloadPlugin(updatePluginId); - // @ts-ignore - await this.app.plugins.loadPlugin(updatePluginId); - Logger(`Plugin reloaded: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId); - }); - })); - - a.appendText(` to reload ${updatePluginName}, or press elsewhere to dismiss this message.`); + this.plugin.askInPopup(`updated-${updatePluginId}`, `Files in ${updatePluginName} has been updated, Press {HERE} to reload ${updatePluginName}, or press elsewhere to dismiss this message.`, (anchor) => { + anchor.text = "HERE"; + anchor.addEventListener("click", async () => { + Logger(`Unloading plugin: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId); + // @ts-ignore + await this.app.plugins.unloadPlugin(updatePluginId); + // @ts-ignore + await this.app.plugins.loadPlugin(updatePluginId); + Logger(`Plugin reloaded: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId); }); - }); - - const updatedPluginKey = "popupUpdated-" + updatePluginId; - scheduleTask(updatedPluginKey, 1000, async () => { - const popup = await memoIfNotExist(updatedPluginKey, () => new Notice(fragment, 0)); - //@ts-ignore - const isShown = popup?.noticeEl?.isShown(); - if (!isShown) { - memoObject(updatedPluginKey, new Notice(fragment, 0)); - } - scheduleTask(updatedPluginKey + "-close", 20000, () => { - const popup = retrieveMemoObject(updatedPluginKey); - if (!popup) - return; - //@ts-ignore - if (popup?.noticeEl?.isShown()) { - popup.hide(); - } - disposeMemoObject(updatedPluginKey); - }); - }); + } + ); } } } catch (ex) { @@ -400,30 +387,11 @@ export class HiddenFileSync extends LiveSyncCommands { // If something changes left, notify for reloading Obsidian. if (updatedCount != 0) { - const fragment = createFragment((doc) => { - doc.createEl("span", null, (a) => { - a.appendText(`Hidden files have been synchronized, Press `); - a.appendChild(a.createEl("a", null, (anchor) => { - anchor.text = "HERE"; - anchor.addEventListener("click", () => { - // @ts-ignore - this.app.commands.executeCommandById("app:reload"); - }); - })); - - a.appendText(` to reload obsidian, or press elsewhere to dismiss this message.`); - }); - }); - - scheduleTask("popupUpdated-" + configDir, 1000, () => { - //@ts-ignore - const isShown = this.confirmPopup?.noticeEl?.isShown(); - if (!isShown) { - this.confirmPopup = new Notice(fragment, 0); - } - scheduleTask("popupClose" + configDir, 20000, () => { - this.confirmPopup?.hide(); - this.confirmPopup = null; + this.plugin.askInPopup(`updated-any-hidden`, `Hidden files have been synchronized, Press {HERE} to reload Obsidian, or press elsewhere to dismiss this message.`, (anchor) => { + anchor.text = "HERE"; + anchor.addEventListener("click", () => { + // @ts-ignore + this.app.commands.executeCommandById("app:reload"); }); }); } @@ -437,6 +405,7 @@ export class HiddenFileSync extends LiveSyncCommands { if (await this.plugin.isIgnoredByIgnoreFiles(file.path)) { return } + const id = await this.path2id(file.path, ICHeader); const prefixedFileName = addPrefix(file.path, ICHeader); const contentBin = await this.app.vault.adapter.readBinary(file.path); @@ -449,7 +418,7 @@ export class HiddenFileSync extends LiveSyncCommands { return false; } const mtime = file.mtime; - return await runWithLock("file-" + prefixedFileName, false, async () => { + return await serialized("file-" + prefixedFileName, async () => { try { const old = await this.localDatabase.getDBEntry(prefixedFileName, null, false, false); let saveData: LoadedEntry; @@ -501,7 +470,7 @@ export class HiddenFileSync extends LiveSyncCommands { if (await this.plugin.isIgnoredByIgnoreFiles(filename)) { return } - await runWithLock("file-" + prefixedFileName, false, async () => { + await serialized("file-" + prefixedFileName, async () => { try { const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, null, true) as InternalFileEntry | false; let saveData: InternalFileEntry; @@ -547,7 +516,7 @@ export class HiddenFileSync extends LiveSyncCommands { if (await this.plugin.isIgnoredByIgnoreFiles(filename)) { return; } - return await runWithLock("file-" + prefixedFileName, false, async () => { + return await serialized("file-" + prefixedFileName, async () => { try { // Check conflicted status //TODO option @@ -618,7 +587,7 @@ export class HiddenFileSync extends LiveSyncCommands { showJSONMergeDialogAndMerge(docA: LoadedEntry, docB: LoadedEntry): Promise { - return runWithLock("conflict:merge-data", false, () => new Promise((res) => { + return serialized("conflict:merge-data", () => new Promise((res) => { Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE); const docs = [docA, docB]; const path = stripAllPrefixes(docA.path); @@ -676,13 +645,16 @@ export class HiddenFileSync extends LiveSyncCommands { } async scanInternalFiles(): Promise { + const configDir = normalizePath(this.app.vault.configDir); const ignoreFilter = this.settings.syncInternalFilesIgnorePatterns .replace(/\n| /g, "") .split(",").filter(e => e).map(e => new RegExp(e, "i")); + const synchronisedInConfigSync = !this.settings.usePluginSync ? [] : Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode == MODE_SELECTIVE || e.mode == MODE_PAUSED).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase()); const root = this.app.vault.getRoot(); const findRoot = root.path; + const filenames = (await this.getFiles(findRoot, [], null, ignoreFilter)).filter(e => e.startsWith(".")).filter(e => !e.startsWith(".trash")); - const files = filenames.map(async (e) => { + const files = filenames.filter(path => synchronisedInConfigSync.every(filterFile => !path.toLowerCase().startsWith(filterFile))).map(async (e) => { return { path: e as FilePath, stat: await this.app.vault.adapter.stat(e) @@ -716,7 +688,7 @@ export class HiddenFileSync extends LiveSyncCommands { ...w.files .filter((e) => !ignoreList.some((ee) => e.endsWith(ee))) .filter((e) => !filter || filter.some((ee) => e.match(ee))) - .filter((e) => !ignoreFilter || ignoreFilter.every((ee) => !e.match(ee))), + .filter((e) => !ignoreFilter || ignoreFilter.every((ee) => !e.match(ee))) ]; let files = [] as string[]; for (const file of filesSrc) { diff --git a/src/CmdPluginAndTheirSettings.ts b/src/CmdPluginAndTheirSettings.ts index fec7937..2c48c0b 100644 --- a/src/CmdPluginAndTheirSettings.ts +++ b/src/CmdPluginAndTheirSettings.ts @@ -9,7 +9,7 @@ import { isPluginMetadata, PeriodicProcessor } from "./utils"; import { PluginDialogModal } from "./dialogs"; import { NewNotice } from "./lib/src/wrapper"; import { versionNumberString2Number } from "./lib/src/strbin"; -import { runWithLock } from "./lib/src/lock"; +import { serialized, skipIfDuplicated } from "./lib/src/lock"; import { LiveSyncCommands } from "./LiveSyncCommands"; export class PluginAndTheirSettings extends LiveSyncCommands { @@ -164,7 +164,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands { if (specificPluginPath != "") { specificPlugin = manifests.find(e => e.dir.endsWith("/" + specificPluginPath))?.id ?? ""; } - await runWithLock("sweepplugin", true, async () => { + await skipIfDuplicated("sweepplugin", async () => { const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO; if (!this.deviceAndVaultName) { Logger("You have to set your device name.", LOG_LEVEL_NOTICE); @@ -223,7 +223,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands { type: "plain" }; Logger(`check diff:${m.name}(${m.id})`, LOG_LEVEL_VERBOSE); - await runWithLock("plugin-" + m.id, false, async () => { + await serialized("plugin-" + m.id, async () => { const old = await this.localDatabase.getDBEntry(p._id as string as FilePathWithPrefix /* This also should be explained */, null, false, false); if (old !== false) { const oldData = { data: old.data, deleted: old._deleted }; @@ -266,7 +266,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands { } async applyPluginData(plugin: PluginDataEntry) { - await runWithLock("plugin-" + plugin.manifest.id, false, async () => { + await serialized("plugin-" + plugin.manifest.id, async () => { const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/"; const adapter = this.app.vault.adapter; // @ts-ignore @@ -288,7 +288,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands { } async applyPlugin(plugin: PluginDataEntry) { - await runWithLock("plugin-" + plugin.manifest.id, false, async () => { + await serialized("plugin-" + plugin.manifest.id, async () => { // @ts-ignore const stat = this.app.plugins.enabledPlugins.has(plugin.manifest.id) == true; if (stat) { diff --git a/src/CmdSetupLiveSync.ts b/src/CmdSetupLiveSync.ts index 92ec0fb..857b83d 100644 --- a/src/CmdSetupLiveSync.ts +++ b/src/CmdSetupLiveSync.ts @@ -20,6 +20,11 @@ export class SetupLiveSync extends LiveSyncCommands { name: "Copy the setup URI", callback: this.command_copySetupURI.bind(this), }); + this.plugin.addCommand({ + id: "livesync-copysetupuri-short", + name: "Copy the setup URI (With customization sync)", + callback: this.command_copySetupURIWithSync.bind(this), + }); this.plugin.addCommand({ id: "livesync-copysetupurifull", @@ -41,11 +46,14 @@ export class SetupLiveSync extends LiveSyncCommands { } async realizeSettingSyncMode() { } - async command_copySetupURI() { + async command_copySetupURI(stripExtra = true) { const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "The passphrase to encrypt the setup URI", "", true); if (encryptingPassphrase === false) return; const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" }; + if (stripExtra) { + delete setting.pluginSyncExtendedSetting; + } const keys = Object.keys(setting) as (keyof ObsidianLiveSyncSettings)[]; for (const k of keys) { if (JSON.stringify(k in setting ? setting[k] : "") == JSON.stringify(k in DEFAULT_SETTINGS ? DEFAULT_SETTINGS[k] : "*")) { @@ -67,6 +75,9 @@ export class SetupLiveSync extends LiveSyncCommands { await navigator.clipboard.writeText(uri); Logger("Setup URI copied to clipboard", LOG_LEVEL_NOTICE); } + async command_copySetupURIWithSync() { + this.command_copySetupURI(false); + } async command_openSetupURI() { const setupURI = await askString(this.app, "Easy setup", "Set up URI", `${configURIBase}aaaaa`); if (setupURI === false) @@ -290,6 +301,7 @@ Of course, we are able to disable these features.` this.plugin.settings.liveSync = false; this.plugin.settings.periodicReplication = false; this.plugin.settings.syncOnSave = false; + this.plugin.settings.syncOnEditorSave = false; this.plugin.settings.syncOnStart = false; this.plugin.settings.syncOnFileOpen = false; this.plugin.settings.syncAfterMerge = false; diff --git a/src/ConflictResolveModal.ts b/src/ConflictResolveModal.ts index 61a45fa..df28afb 100644 --- a/src/ConflictResolveModal.ts +++ b/src/ConflictResolveModal.ts @@ -18,10 +18,8 @@ export class ConflictResolveModal extends Modal { onOpen() { const { contentEl } = this; - + this.titleEl.setText("Conflicting changes"); contentEl.empty(); - - contentEl.createEl("h2", { text: "This document has conflicted changes." }); contentEl.createEl("span", { text: this.filename }); const div = contentEl.createDiv(""); div.addClass("op-scrollable"); diff --git a/src/DocumentHistoryModal.ts b/src/DocumentHistoryModal.ts index cd5e569..7721caf 100644 --- a/src/DocumentHistoryModal.ts +++ b/src/DocumentHistoryModal.ts @@ -10,29 +10,29 @@ import { stripPrefix } from "./lib/src/path"; export class DocumentHistoryModal extends Modal { plugin: ObsidianLiveSyncPlugin; - range: HTMLInputElement; - contentView: HTMLDivElement; - info: HTMLDivElement; - fileInfo: HTMLDivElement; + range!: HTMLInputElement; + contentView!: HTMLDivElement; + info!: HTMLDivElement; + fileInfo!: HTMLDivElement; showDiff = false; - id: DocumentID; + id?: DocumentID; file: FilePathWithPrefix; revs_info: PouchDB.Core.RevisionInfo[] = []; - currentDoc: LoadedEntry; + currentDoc?: LoadedEntry; currentText = ""; currentDeleted = false; - initialRev: string; + initialRev?: string; - constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile | FilePathWithPrefix, id: DocumentID, revision?: string) { + constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile | FilePathWithPrefix, id?: DocumentID, revision?: string) { super(app); this.plugin = plugin; this.file = (file instanceof TFile) ? getPathFromTFile(file) : file; this.id = id; this.initialRev = revision; - if (!file) { - this.file = this.plugin.id2path(id, null); + if (!file && id) { + this.file = this.plugin.id2path(id); } if (localStorage.getItem("ols-history-highlightdiff") == "1") { this.showDiff = true; @@ -46,8 +46,8 @@ export class DocumentHistoryModal extends Modal { const db = this.plugin.localDatabase; try { const w = await db.localDatabase.get(this.id, { revs_info: true }); - this.revs_info = w._revs_info.filter((e) => e?.status == "available"); - this.range.max = `${this.revs_info.length - 1}`; + this.revs_info = w._revs_info?.filter((e) => e?.status == "available") ?? []; + this.range.max = `${Math.max(this.revs_info.length - 1, 0)}`; this.range.value = this.range.max; this.fileInfo.setText(`${this.file} / ${this.revs_info.length} revisions`); await this.loadRevs(initialRev); @@ -90,7 +90,7 @@ export class DocumentHistoryModal extends Modal { this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`; let result = ""; const w1data = w.datatype == "plain" ? getDocData(w.data) : base64ToString(w.data); - this.currentDeleted = w.deleted; + this.currentDeleted = !!w.deleted; this.currentText = w1data; if (this.showDiff) { const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1); @@ -130,9 +130,8 @@ export class DocumentHistoryModal extends Modal { onOpen() { const { contentEl } = this; - + this.titleEl.setText("Document History"); contentEl.empty(); - contentEl.createEl("h2", { text: "Document History" }); this.fileInfo = contentEl.createDiv(""); this.fileInfo.addClass("op-info"); const divView = contentEl.createDiv(""); diff --git a/src/JsonResolveModal.ts b/src/JsonResolveModal.ts index 60998fa..64d68ef 100644 --- a/src/JsonResolveModal.ts +++ b/src/JsonResolveModal.ts @@ -29,7 +29,7 @@ export class JsonResolveModal extends Modal { onOpen() { const { contentEl } = this; - + this.titleEl.setText("Conflicted Setting"); contentEl.empty(); if (this.component == null) { diff --git a/src/JsonResolvePane.svelte b/src/JsonResolvePane.svelte index 6a67192..e814966 100644 --- a/src/JsonResolvePane.svelte +++ b/src/JsonResolvePane.svelte @@ -104,7 +104,6 @@ ] as ["" | "A" | "B" | "AB" | "BA", string][]; -

Conflicted settings

{filename}

{#if !docA || !docB}
Just for a minute, please!
diff --git a/src/LogDisplayModal.ts b/src/LogDisplayModal.ts index 2d24e18..4ec52ac 100644 --- a/src/LogDisplayModal.ts +++ b/src/LogDisplayModal.ts @@ -14,9 +14,9 @@ export class LogDisplayModal extends Modal { onOpen() { const { contentEl } = this; + this.titleEl.setText("Sync status"); contentEl.empty(); - contentEl.createEl("h2", { text: "Sync Status" }); const div = contentEl.createDiv(""); div.addClass("op-scrollable"); div.addClass("op-pre"); diff --git a/src/ObsidianLiveSyncSettingTab.ts b/src/ObsidianLiveSyncSettingTab.ts index 2d76027..4e48a4a 100644 --- a/src/ObsidianLiveSyncSettingTab.ts +++ b/src/ObsidianLiveSyncSettingTab.ts @@ -120,6 +120,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { if (this.plugin.settings.periodicReplication) return true; if (this.plugin.settings.syncOnFileOpen) return true; if (this.plugin.settings.syncOnSave) return true; + if (this.plugin.settings.syncOnEditorSave) return true; if (this.plugin.settings.syncOnStart) return true; if (this.plugin.settings.syncAfterMerge) return true; if (this.plugin.replicator.syncStatus == "CONNECTED") return true; @@ -157,6 +158,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { this.plugin.settings.liveSync = false; this.plugin.settings.periodicReplication = false; this.plugin.settings.syncOnSave = false; + this.plugin.settings.syncOnEditorSave = false; this.plugin.settings.syncOnStart = false; this.plugin.settings.syncOnFileOpen = false; this.plugin.settings.syncAfterMerge = false; @@ -216,7 +218,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { syncLive.forEach((e) => { e.setDisabled(false).setTooltip(""); }); - } else if (this.plugin.settings.syncOnFileOpen || this.plugin.settings.syncOnSave || this.plugin.settings.syncOnStart || this.plugin.settings.periodicReplication || this.plugin.settings.syncAfterMerge) { + } else if (this.plugin.settings.syncOnFileOpen || this.plugin.settings.syncOnSave || this.plugin.settings.syncOnEditorSave || this.plugin.settings.syncOnStart || this.plugin.settings.periodicReplication || this.plugin.settings.syncAfterMerge) { syncNonLive.forEach((e) => { e.setDisabled(false).setTooltip(""); }); @@ -891,6 +893,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { liveSync: false, periodicReplication: false, syncOnSave: false, + syncOnEditorSave: false, syncOnStart: false, syncOnFileOpen: false, syncAfterMerge: false, @@ -904,6 +907,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { batchSave: true, periodicReplication: true, syncOnSave: false, + syncOnEditorSave: false, syncOnStart: true, syncOnFileOpen: true, syncAfterMerge: true, @@ -1014,6 +1018,17 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { applyDisplayEnabled(); }) ) + new Setting(containerSyncSettingEl) + .setName("Sync on Editor Save") + .setDesc("When you save file on the editor, sync automatically") + .setClass("wizardHidden") + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.syncOnEditorSave).onChange(async (value) => { + this.plugin.settings.syncOnEditorSave = value; + await this.plugin.saveSettings(); + applyDisplayEnabled(); + }) + ) new Setting(containerSyncSettingEl) .setName("Sync on File Open") .setDesc("When you open file, sync automatically") @@ -1199,7 +1214,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, \\/obsidian-livesync\\/"; const defaultSkipPatternXPlat = defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$"; new Setting(containerSyncSettingEl) - .setName("Skip patterns") + .setName("Folders and files to ignore") .setDesc( "Regular expression, If you use hidden file sync between desktop and mobile, adding `workspace$` is recommended." ) @@ -1777,8 +1792,8 @@ ${stringifyYaml(pluginConfig)}`; dropdown .addOptions({ "": "Old Algorithm", "xxhash32": "xxhash32 (Fast)", "xxhash64": "xxhash64 (Fastest)" } as Record) .setValue(this.plugin.settings.hashAlg) - .onChange(async (value: HashAlgorithm) => { - this.plugin.settings.hashAlg = value; + .onChange(async (value) => { + this.plugin.settings.hashAlg = value as HashAlgorithm; await this.plugin.saveSettings(); }) ) diff --git a/src/PluginCombo.svelte b/src/PluginCombo.svelte index 5158230..0dbdd11 100644 --- a/src/PluginCombo.svelte +++ b/src/PluginCombo.svelte @@ -108,7 +108,7 @@ } } }) - .reduce((p, c) => p | c, 0); + .reduce((p, c) => p | (c as number), 0 as number); if (matchingStatus == 0b0000100) { equivalency = "⚖️ Same"; canApply = false; diff --git a/src/PluginPane.svelte b/src/PluginPane.svelte index 9eebda6..cd1c0b9 100644 --- a/src/PluginPane.svelte +++ b/src/PluginPane.svelte @@ -3,9 +3,13 @@ import ObsidianLiveSyncPlugin from "./main"; import { type PluginDataExDisplay, pluginIsEnumerating, pluginList } from "./CmdConfigSync"; import PluginCombo from "./PluginCombo.svelte"; + import { Menu } from "obsidian"; + import { unique } from "./lib/src/utils"; + import { MODE_SELECTIVE, MODE_AUTOMATIC, MODE_PAUSED, type SYNC_MODE, type PluginSyncSettingEntry } from "./lib/src/types"; + import { normalizePath } from "./deps"; export let plugin: ObsidianLiveSyncPlugin; - $: hideNotApplicable = true; + $: hideNotApplicable = false; $: thisTerm = plugin.deviceAndVaultName; const addOn = plugin.addOnConfigSync; @@ -13,7 +17,7 @@ let list: PluginDataExDisplay[] = []; let selectNewestPulse = 0; - let hideEven = true; + let hideEven = false; let loading = false; let applyAllPluse = 0; let isMaintenanceMode = false; @@ -80,6 +84,54 @@ async function deleteData(data: PluginDataExDisplay): Promise { return await addOn.deleteData(data); } + function askMode(evt: MouseEvent, title: string, key: string) { + const menu = new Menu(); + menu.addItem((item) => item.setTitle(title).setIsLabel(true)); + menu.addSeparator(); + const prevMode = automaticList.get(key) ?? MODE_SELECTIVE; + for (const mode of [MODE_SELECTIVE, MODE_AUTOMATIC, MODE_PAUSED]) { + menu.addItem((item) => { + item.setTitle(`${getIcon(mode as SYNC_MODE)}:${TITLES[mode]}`) + .onClick((e) => { + if (mode === MODE_AUTOMATIC) { + askOverwriteModeForAutomatic(evt, key); + } else { + setMode(key, mode as SYNC_MODE); + } + }) + .setChecked(prevMode == mode) + .setDisabled(prevMode == mode); + }); + } + menu.showAtMouseEvent(evt); + } + function applyAutomaticSync(key: string, direction: "pushForce" | "pullForce" | "safe") { + setMode(key, MODE_AUTOMATIC); + const configDir = normalizePath(plugin.app.vault.configDir); + const files = (plugin.settings.pluginSyncExtendedSetting[key]?.files ?? []).map((e) => `${configDir}/${e}`); + plugin.addOnHiddenFileSync.syncInternalFilesAndDatabase(direction, true, false, files); + } + function askOverwriteModeForAutomatic(evt: MouseEvent, key: string) { + const menu = new Menu(); + menu.addItem((item) => item.setTitle("Initial Action").setIsLabel(true)); + menu.addSeparator(); + menu.addItem((item) => { + item.setTitle(`↑: Overwrite Remote`).onClick((e) => { + applyAutomaticSync(key, "pushForce"); + }); + }) + .addItem((item) => { + item.setTitle(`↓: Overwrite Local`).onClick((e) => { + applyAutomaticSync(key, "pullForce"); + }); + }) + .addItem((item) => { + item.setTitle(`⇅: Use newer`).onClick((e) => { + applyAutomaticSync(key, "safe"); + }); + }); + menu.showAtMouseEvent(evt); + } $: options = { thisTerm, @@ -92,11 +144,84 @@ plugin, isMaintenanceMode, }; + + const ICON_EMOJI_PAUSED = `⛔`; + const ICON_EMOJI_AUTOMATIC = `✨`; + const ICON_EMOJI_SELECTIVE = `🔀`; + + const ICONS: { [key: number]: string } = { + [MODE_SELECTIVE]: ICON_EMOJI_SELECTIVE, + [MODE_PAUSED]: ICON_EMOJI_PAUSED, + [MODE_AUTOMATIC]: ICON_EMOJI_AUTOMATIC, + }; + const TITLES: { [key: number]: string } = { + [MODE_SELECTIVE]: "Selective", + [MODE_PAUSED]: "Ignore", + [MODE_AUTOMATIC]: "Automatic", + }; + const PREFIX_PLUGIN_ALL = "PLUGIN_ALL"; + const PREFIX_PLUGIN_DATA = "PLUGIN_DATA"; + const PREFIX_PLUGIN_MAIN = "PLUGIN_MAIN"; + function setMode(key: string, mode: SYNC_MODE) { + if (key.startsWith(PREFIX_PLUGIN_ALL + "/")) { + setMode(PREFIX_PLUGIN_DATA + key.substring(PREFIX_PLUGIN_ALL.length), mode); + setMode(PREFIX_PLUGIN_MAIN + key.substring(PREFIX_PLUGIN_ALL.length), mode); + } + const files = unique( + list + .filter((e) => `${e.category}/${e.name}` == key) + .map((e) => e.files) + .flat() + .map((e) => e.filename) + ); + automaticList.set(key, mode); + automaticListDisp = automaticList; + if (!(key in plugin.settings.pluginSyncExtendedSetting)) { + plugin.settings.pluginSyncExtendedSetting[key] = { + key, + mode, + files: [], + }; + } + plugin.settings.pluginSyncExtendedSetting[key].files = files; + plugin.settings.pluginSyncExtendedSetting[key].mode = mode; + plugin.saveSettingData(); + } + function getIcon(mode: SYNC_MODE) { + if (mode in ICONS) { + return ICONS[mode]; + } else { + (""); + } + } + let automaticList = new Map(); + let automaticListDisp = new Map(); + + // apply current configuration to the dialogue + for (const { key, mode } of Object.values(plugin.settings.pluginSyncExtendedSetting)) { + automaticList.set(key, mode); + } + + automaticListDisp = automaticList; + + let displayKeys: Record = {}; + + $: { + const extraKeys = Object.keys(plugin.settings.pluginSyncExtendedSetting); + displayKeys = [ + ...list, + ...extraKeys + .map((e) => `${e}///`.split("/")) + .filter((e) => e[0] && e[1]) + .map((e) => ({ category: e[0], name: e[1], displayName: e[1] })), + ] + .sort((a, b) => (a.displayName ?? a.name).localeCompare(b.displayName ?? b.name)) + .reduce((p, c) => ({ ...p, [c.category]: unique(c.category in p ? [...p[c.category], c.displayName ?? c.name] : [c.displayName ?? c.name]) }), {} as Record); + }
-

Customization sync

@@ -119,15 +244,24 @@ {#if list.length == 0}
No Items.
{:else} - {#each Object.entries(displays) as [key, label]} + {#each Object.entries(displays).filter(([key, _]) => key in displayKeys) as [key, label]}

{label}

- {#each groupBy(filterList(list, [key]), "name") as [name, listX]} + {#each displayKeys[key] as name} + {@const bindKey = `${key}/${name}`} + {@const mode = automaticListDisp.get(bindKey) ?? MODE_SELECTIVE}
- {name} + + {name}
-
{/each}
@@ -135,20 +269,55 @@

Plugins

{#each groupBy(filterList(list, ["PLUGIN_MAIN", "PLUGIN_DATA", "PLUGIN_ETC"]), "name") as [name, listX]} + {@const bindKeyAll = `${PREFIX_PLUGIN_ALL}/${name}`} + {@const modeAll = automaticListDisp.get(bindKeyAll) ?? MODE_SELECTIVE} + {@const bindKeyMain = `${PREFIX_PLUGIN_MAIN}/${name}`} + {@const modeMain = automaticListDisp.get(bindKeyMain) ?? MODE_SELECTIVE} + {@const bindKeyData = `${PREFIX_PLUGIN_DATA}/${name}`} + {@const modeData = automaticListDisp.get(bindKeyData) ?? MODE_SELECTIVE}
- {name} + + {name}
-
-
-
Main
-
-
-
Data
-
+ {#if modeAll == MODE_SELECTIVE} +
+
+ + MAIN +
+ {#if modeMain == MODE_SELECTIVE} +
+
+
+ + DATA +
+ {#if modeData == MODE_SELECTIVE} +
+ {:else} +
+
{TITLES[modeAll]}
+
+ {/if} {/each}
{/if} @@ -162,6 +331,15 @@