From e77031f1cd7d574b1c49505aaff3faadba050997 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 28 Apr 2023 13:32:58 +0900 Subject: [PATCH] Implemented: - New feature `Customization sync` has replaced `Plugin and their settings` --- src/CmdConfigSync.ts | 651 ++++++++++++++++++++++++++++++ src/CmdHiddenFileSync.ts | 15 +- src/CmdPluginAndTheirSettings.ts | 3 +- src/CmdSetupLiveSync.ts | 70 +++- src/JsonResolveModal.ts | 11 +- src/JsonResolvePane.svelte | 40 +- src/ObsidianLiveSyncSettingTab.ts | 24 +- src/PluginCombo.svelte | 314 ++++++++++++++ src/PluginPane.svelte | 452 +++++++++------------ src/StorageEventManager.ts | 2 +- src/deps.ts | 1 + src/dialogs.ts | 5 +- src/lib | 2 +- src/main.ts | 24 +- src/types.ts | 8 + src/utils.ts | 11 +- 16 files changed, 1291 insertions(+), 342 deletions(-) create mode 100644 src/CmdConfigSync.ts create mode 100644 src/PluginCombo.svelte diff --git a/src/CmdConfigSync.ts b/src/CmdConfigSync.ts new file mode 100644 index 0000000..545cd96 --- /dev/null +++ b/src/CmdConfigSync.ts @@ -0,0 +1,651 @@ +import { writable } from 'svelte/store'; +import { Notice, PluginManifest, stringifyYaml, parseYaml } from "./deps"; + +import { EntryDoc, LoadedEntry, LOG_LEVEL, InternalFileEntry, FilePathWithPrefix, FilePath, DocumentID } 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 { PouchDB } from "./lib/src/pouchdb-browser.js"; +import { WrappedNotice } from "./lib/src/wrapper"; +import { base64ToArrayBuffer, arrayBufferToBase64, readString, writeString, uint8ArrayToHexString } from "./lib/src/strbin"; +import { runWithLock } from "./lib/src/lock"; +import { LiveSyncCommands } from "./LiveSyncCommands"; +import { addPrefix, stripAllPrefixes } from "./lib/src/path"; +import { PeriodicProcessor, askYesNo, disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "./utils"; +import { Semaphore } from "./lib/src/semaphore"; +import { PluginDialogModal } from "./dialogs"; +import { JsonResolveModal } from "./JsonResolveModal"; + + + + +export const pluginList = writable([] as PluginDataExDisplay[]); +export const pluginIsEnumerating = writable(false); + +const hashString = (async (key: string) => { + const buff = writeString(key); + const digest = await crypto.subtle.digest('SHA-256', buff); + return uint8ArrayToHexString(new Uint8Array(digest)); +}) + +export type PluginDataExFile = { + filename: string, + data?: string[], + mtime: number, + size: number, + version?: string, + displayName?: string, +} +export type PluginDataExDisplay = { + documentPath: FilePathWithPrefix, + category: string, + name: string, + term: string, + displayName?: string, + files: PluginDataExFile[], + version?: string, + mtime: number, +} +export type PluginDataEx = { + documentPath?: FilePathWithPrefix, + category: string, + name: string, + displayName?: string, + term: string, + files: PluginDataExFile[], + version?: string, + mtime: number, +}; +export class ConfigSync extends LiveSyncCommands { + confirmPopup: WrappedNotice = null; + get kvDB() { + return this.plugin.kvDB; + } + ensureDirectoryEx(fullPath: string) { + return this.plugin.ensureDirectoryEx(fullPath); + } + pluginDialog: PluginDialogModal = null; + periodicPluginSweepProcessor = new PeriodicProcessor(this.plugin, async () => await this.scanAllConfigFiles(false)); + + showPluginSyncModal() { + if (!this.settings.usePluginSync) { + return; + } + if (this.pluginDialog != null) { + this.pluginDialog.open(); + } else { + this.pluginDialog = new PluginDialogModal(this.app, this.plugin); + this.pluginDialog.open(); + } + } + + hidePluginSyncModal() { + if (this.pluginDialog != null) { + this.pluginDialog.close(); + this.pluginDialog = null; + } + } + onunload() { + this.hidePluginSyncModal(); + this.periodicPluginSweepProcessor?.disable(); + } + onload() { + this.plugin.addCommand({ + id: "livesync-plugin-dialog-ex", + name: "Show customization sync dialog", + callback: () => { + this.showPluginSyncModal(); + }, + }); + } + 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"; + if (filePath.startsWith(`${this.app.vault.configDir}/snippets/`) && filePath.endsWith(".css")) return "SNIPPET"; + if (filePath.startsWith(`${this.app.vault.configDir}/plugins/`)) { + if (filePath.endsWith("/styles.css") || filePath.endsWith("/manifest.json") || filePath.endsWith("/main.js")) { + return "PLUGIN_MAIN"; + } else if (filePath.endsWith("/data.json")) { + return "PLUGIN_DATA"; + } else { + //TODO: to be configurable. + // With algorithm which implemented at v0.19.0, is too heavy. + return ""; + // return "PLUGIN_ETC"; + } + // return "PLUGIN"; + } + return ""; + } + isTargetPath(filePath: string): boolean { + if (!filePath.startsWith(this.app.vault.configDir)) return false; + // Idea non-filter option? + return this.getFileCategory(filePath) != ""; + } + async onInitializeDatabase(showNotice: boolean) { + if (this.settings.usePluginSync) { + try { + Logger("Scanning customizations..."); + await this.scanAllConfigFiles(showNotice); + Logger("Scanning customizations : done"); + } catch (ex) { + Logger("Scanning customizations : failed"); + Logger(ex, LOG_LEVEL.VERBOSE); + } + + } + } + async beforeReplicate(showNotice: boolean) { + if (this.settings.autoSweepPlugins && this.settings.usePluginSync) { + await this.scanAllConfigFiles(showNotice); + } + } + async onResume() { + if (this.plugin.suspended) + return; + if (this.settings.autoSweepPlugins) { + await this.scanAllConfigFiles(true); + } + this.periodicPluginSweepProcessor.enable(this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges ? (PERIODIC_PLUGIN_SWEEP * 1000) : 0); + } + + async reloadPluginList() { + pluginList.set([]) + await this.updatePluginList(); + } + async updatePluginList(updatedDocumentPath?: FilePathWithPrefix): Promise { + // pluginList.set([]); + if (!this.settings.usePluginSync) { + pluginList.set([]); + return; + } + + await runWithLock("update-plugin-list", false, async () => { + if (updatedDocumentPath != "") pluginList.update(e => e.filter(ee => ee.documentPath != updatedDocumentPath)); + // const work: Record>>> = {}; + const entries = [] as PluginDataExDisplay[] + const plugins = this.localDatabase.findEntries(ICXHeader + "", `${ICXHeader}\u{10ffff}`, { include_docs: true }); + const semaphore = Semaphore(4); + const processes = [] as Promise[]; + let count = 0; + pluginIsEnumerating.set(true); + try { + for await (const plugin of plugins) { + const path = plugin.path || this.getPath(plugin); + if (updatedDocumentPath && updatedDocumentPath != path) { + continue; + } + processes.push((async (v) => { + const release = await semaphore.acquire(1); + try { + Logger(`Enumerating files... ${count++}`, LOG_LEVEL.NOTICE, "get-plugins"); + + Logger(`plugin-${path}`, LOG_LEVEL.VERBOSE); + const wx = await this.localDatabase.getDBEntry(path, null, false, false); + if (wx) { + const data = parseYaml(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 = [await hashString(tempStr)]; + xFiles.push(work); + } + entries.push({ + ...data, + documentPath: this.getPath(wx), + files: xFiles + }); + } + } catch (ex) { + //TODO + Logger(`Something happened at enumerating customization :${v.path}`); + console.warn(ex); + } finally { + release(); + } + } + )(plugin)); + } + await Promise.all(processes); + pluginList.update(e => { + let newList = [...e]; + for (const item of entries) { + console.log(item.documentPath); + newList = newList.filter(x => x.documentPath != item.documentPath); + newList.push(item) + } + return newList; + }) + Logger(`All files enumerated`, LOG_LEVEL.NOTICE, "get-plugins"); + } finally { + pluginIsEnumerating.set(false); + } + }); + // return entries; + } + async compareUsingDisplayData(dataA: PluginDataExDisplay, dataB: PluginDataExDisplay) { + const docA = await this.localDatabase.getDBEntry(dataA.documentPath); + const docB = await this.localDatabase.getDBEntry(dataB.documentPath); + + if (docA && docB) { + const pluginDataA = parseYaml(getDocData(docA.data)) as PluginDataEx; + pluginDataA.documentPath = dataA.documentPath; + const pluginDataB = parseYaml(getDocData(docB.data)) as PluginDataEx; + pluginDataB.documentPath = dataB.documentPath; + + // Use outer structure to wrap each data. + return await this.showJSONMergeDialogAndMerge(docA, docB, pluginDataA, pluginDataB); + + } + return false; + } + showJSONMergeDialogAndMerge(docA: LoadedEntry, docB: LoadedEntry, pluginDataA: PluginDataEx, pluginDataB: PluginDataEx): Promise { + 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) => { + Logger("Opening data-merging dialog", LOG_LEVEL.VERBOSE); + // const docs = [docA, docB]; + const path = stripAllPrefixes(docAx.path.split("/").slice(-1).join("/") as FilePath); + const modal = new JsonResolveModal(this.app, path, [docAx, docBx], async (keep, result) => { + if (result == null) return res(false); + try { + res(await this.applyData(pluginDataA, result)); + } catch (ex) { + Logger("Could not apply merged file"); + Logger(ex, LOG_LEVEL.VERBOSE); + res(false); + } + }, "📡", "🛰️", "B"); + modal.open(); + })); + } + async applyData(data: PluginDataEx, content?: string): Promise { + Logger(`Applying ${data.displayName || data.name}..`); + const baseDir = this.app.vault.configDir; + try { + if (!data.documentPath) throw "InternalError: Document path not exist"; + const dx = await this.localDatabase.getDBEntry(data.documentPath); + if (dx == false) { + throw "Not found on database" + } + const loadedData = parseYaml(getDocData(dx.data)) as PluginDataEx; + for (const f of loadedData.files) { + Logger(`Applying ${f.filename} of ${data.displayName || data.name}..`); + try { + // console.dir(f); + const path = `${baseDir}/${f.filename}`; + await this.ensureDirectoryEx(path); + if (!content) { + const dt = base64ToArrayBuffer(f.data); + await this.app.vault.adapter.writeBinary(path, dt); + } else { + await this.app.vault.adapter.write(path, content); + } + Logger(`Applying ${f.filename} of ${data.displayName || data.name}.. Done`); + + } catch (ex) { + Logger(`Applying ${f.filename} of ${data.displayName || data.name}.. Failed`); + Logger(ex, LOG_LEVEL.VERBOSE); + } + + } + const uPath = `${baseDir}/${loadedData.files[0].filename}` as FilePath; + await this.storeCustomizationFiles(uPath); + await this.updatePluginList(uPath); + await delay(100); + Logger(`Config ${data.displayName || data.name} has been applied`, LOG_LEVEL.NOTICE); + if (data.category == "PLUGIN_DATA" || data.category == "PLUGIN_MAIN") { + //@ts-ignore + const manifests = Object.values(this.app.plugins.manifests) as any as PluginManifest[]; + //@ts-ignore + const enabledPlugins = this.app.plugins.enabledPlugins as Set; + const pluginManifest = manifests.find((manifest) => enabledPlugins.has(manifest.id) && manifest.dir == `${baseDir}/plugins/${data.name}`); + if (pluginManifest) { + Logger(`Unloading plugin: ${pluginManifest.name}`, LOG_LEVEL.NOTICE, "plugin-reload-" + pluginManifest.id); + // @ts-ignore + await this.app.plugins.unloadPlugin(pluginManifest.id); + // @ts-ignore + await this.app.plugins.loadPlugin(pluginManifest.id); + Logger(`Plugin reloaded: ${pluginManifest.name}`, LOG_LEVEL.NOTICE, "plugin-reload-" + pluginManifest.id); + } + } else if (data.category == "CONFIG") { + scheduleTask("configReload", 250, async () => { + if (await askYesNo(this.app, "Do you want to restart and reload Obsidian now?") == "yes") { + // @ts-ignore + this.app.commands.executeCommandById("app:reload") + } + }) + } + return true; + } catch (ex) { + Logger(`Applying ${data.displayName || data.name}.. Failed`); + Logger(ex, LOG_LEVEL.VERBOSE); + return false; + } + } + async deleteData(data: PluginDataEx): Promise { + try { + if (data.documentPath) { + await this.deleteConfigOnDatabase(data.documentPath); + Logger(`Delete: ${data.documentPath}`, LOG_LEVEL.NOTICE); + } + return true; + } catch (ex) { + Logger(`Failed to delete: ${data.documentPath}`, LOG_LEVEL.NOTICE); + return false; + + } + } + parseReplicationResultItem(docs: PouchDB.Core.ExistingDocument) { + if (docs._id.startsWith(ICXHeader)) { + if (this.plugin.settings.usePluginSync && this.plugin.settings.notifyPluginOrSettingUpdated) { + if (!this.pluginDialog || (this.pluginDialog && !this.pluginDialog.isOpened())) { + const fragment = createFragment((doc) => { + doc.createEl("span", null, (a) => { + a.appendText(`Some configuration has been arrived, Press `); + a.appendChild(a.createEl("a", null, (anchor) => { + anchor.text = "HERE"; + anchor.addEventListener("click", async () => { + this.showPluginSyncModal(); + }); + })); + + a.appendText(` to open the config sync dialog , or press elsewhere to dismiss this message.`); + }); + }); + + const updatedPluginKey = "popupUpdated-plugins"; + 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); + }); + }); + } else { + this.updatePluginList(docs.path ? docs.path : this.getPath(docs)); + } + + } + return true; + } + return false; + } + async realizeSettingSyncMode(): Promise { + this.periodicPluginSweepProcessor?.disable(); + if (this.plugin.suspended) + return; + if (!this.settings.usePluginSync) { + return; + } + if (this.settings.autoSweepPlugins) { + await this.scanAllConfigFiles(false); + } + 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); + let version: string | undefined; + let displayName: string | undefined; + if (!stat) { + return false; + } + const contentBin = await this.app.vault.adapter.readBinary(path); + let content: string[]; + try { + content = await arrayBufferToBase64(contentBin); + if (path.toLowerCase().endsWith("/manifest.json")) { + const v = readString(new Uint8Array(contentBin)); + try { + const json = JSON.parse(v); + if ("version" in json) { + version = `${json.version}`; + } + if ("name" in json) { + displayName = `${json.name}`; + } + } catch (ex) { + Logger(`Configuration sync data: ${path} looks like manifest, but could not read the version`, LOG_LEVEL.INFO); + } + } + } catch (ex) { + Logger(`The file ${path} could not be encoded`); + Logger(ex, LOG_LEVEL.VERBOSE); + return false; + } + const mtime = stat.mtime; + return { + filename: path.substring(this.app.vault.configDir.length + 1), + data: content, + mtime, + size: stat.size, + version, + displayName: displayName, + } + } + + filenameToUnifiedKey(path: string, termOverRide?: string) { + const term = termOverRide || this.plugin.deviceAndVaultName; + const category = this.getFileCategory(path); + const name = (category == "CONFIG" || category == "SNIPPET") ? + (path.split("/").slice(-1)[0]) : + (category == "PLUGIN_ETC" ? + path.split("/").slice(-2).join("/") : + path.split("/").slice(-2)[0]); + return `${ICXHeader}${term}/${category}/${name}.md` as FilePathWithPrefix + } + async storeCustomizationFiles(path: FilePath, termOverRide?: string) { + const term = termOverRide || this.plugin.deviceAndVaultName; + const vf = this.filenameToUnifiedKey(path, term); + return await runWithLock(`plugin-${vf}`, false, async () => { + const category = this.getFileCategory(path); + let mtime = 0; + let fileTargets = [] as FilePath[]; + // let savePath = ""; + const name = (category == "CONFIG" || category == "SNIPPET") ? + (path.split("/").reverse()[0]) : + (path.split("/").reverse()[1]); + const parentPath = path.split("/").slice(0, -1).join("/"); + const prefixedFileName = this.filenameToUnifiedKey(path, term); + const id = await this.path2id(prefixedFileName); + const dt: PluginDataEx = { + category: category, + files: [], + name: name, + mtime: 0, + term: term + } + // let scheduleKey = ""; + if (category == "CONFIG" || category == "SNIPPET" || category == "PLUGIN_ETC" || category == "PLUGIN_DATA") { + fileTargets = [path]; + if (category == "PLUGIN_ETC") { + dt.displayName = path.split("/").slice(-1).join("/"); + } + } else if (category == "PLUGIN_MAIN") { + fileTargets = ["manifest.json", "main.js", "styles.css"].map(e => `${parentPath}/${e}` as FilePath); + } else if (category == "THEME") { + fileTargets = ["manifest.json", "theme.css"].map(e => `${parentPath}/${e}` as FilePath); + } + for (const target of fileTargets) { + const data = await this.makeEntryFromFile(target); + if (data == false) { + Logger(`Config: skipped: ${target} `, LOG_LEVEL.VERBOSE); + continue; + } + if (data.version) { + dt.version = data.version; + } + if (data.displayName) { + dt.displayName = data.displayName; + } + // Use average for total modified time. + mtime = mtime == 0 ? data.mtime : ((data.mtime + mtime) / 2); + dt.files.push(data); + } + dt.mtime = mtime; + + // Logger(`Configuration saving: ${prefixedFileName}`); + if (dt.files.length == 0) { + Logger(`Nothing left: deleting.. ${path}`); + return await this.deleteConfigOnDatabase(prefixedFileName); + } + + const content = stringifyYaml(dt); + try { + const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, null, false); + let saveData: LoadedEntry; + if (old === false) { + saveData = { + _id: id, + path: prefixedFileName, + data: content, + mtime, + ctime: mtime, + datatype: "newnote", + size: content.length, + children: [], + deleted: false, + type: "newnote", + }; + } else { + if (old.mtime == mtime) { + // Logger(`STORAGE --> DB:${file.path}: (hidden) Not changed`, LOG_LEVEL.VERBOSE); + return true; + } + saveData = + { + ...old, + data: content, + mtime, + size: content.length, + datatype: "newnote", + children: [], + deleted: false, + type: "newnote", + }; + } + const ret = await this.localDatabase.putDBEntry(saveData); + Logger(`STORAGE --> DB:${prefixedFileName}: (config) Done`); + return ret; + } catch (ex) { + Logger(`STORAGE --> DB:${prefixedFileName}: (config) Failed`); + Logger(ex, LOG_LEVEL.VERBOSE); + return false; + } + }) + // }) + + } + async watchVaultRawEventsAsync(path: FilePath) { + if (!this.isTargetPath(path)) return false; + const stat = await this.app.vault.adapter.stat(path); + // Make sure that target is a file. + if (stat && stat.type != "file") + return false; + const storageMTime = ~~((stat && stat.mtime || 0) / 1000); + const key = `${path}-${storageMTime}`; + if (this.recentProcessedInternalFiles.contains(key)) { + // If recently processed, it may caused by self. + return true; + } + this.recentProcessedInternalFiles = [key, ...this.recentProcessedInternalFiles].slice(0, 100); + + this.storeCustomizationFiles(path).then(() => {/* Fire and forget */ }); + + } + + + async scanAllConfigFiles(showMessage: boolean) { + const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO; + Logger("Scanning customizing files.", logLevel, "scan-all-config"); + const term = this.plugin.deviceAndVaultName; + if (term == "") { + Logger("We have to configure the device name", LOG_LEVEL.NOTICE); + return; + } + const filesAll = await this.scanInternalFiles(); + const files = filesAll.filter(e => this.isTargetPath(e)).map(e => ({ key: this.filenameToUnifiedKey(e), file: e })); + const virtualPathsOfLocalFiles = [...new Set(files.map(e => e.key))]; + const filesOnDB = ((await this.localDatabase.allDocsRaw({ startkey: ICXHeader + "", endkey: `${ICXHeader}\u{10ffff}`, include_docs: true })).rows.map(e => e.doc) as InternalFileEntry[]).filter(e => !e.deleted); + let deleteCandidate = filesOnDB.map(e => this.getPath(e)).filter(e => e.startsWith(`${ICXHeader}${term}/`)); + for (const vp of virtualPathsOfLocalFiles) { + const p = files.find(e => e.key == vp).file; + await this.storeCustomizationFiles(p); + deleteCandidate = deleteCandidate.filter(e => e != vp); + } + for (const vp of deleteCandidate) { + await this.deleteConfigOnDatabase(vp); + } + } + async deleteConfigOnDatabase(prefixedFileName: FilePathWithPrefix, forceWrite = false) { + + // const id = await this.path2id(prefixedFileName); + const mtime = new Date().getTime(); + await runWithLock("file-x-" + prefixedFileName, false, async () => { + try { + const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, null, false) as InternalFileEntry | false; + let saveData: InternalFileEntry; + if (old === false) { + Logger(`STORAGE -x> DB:${prefixedFileName}: (config) already deleted (Not found on database)`); + } else { + if (old.deleted) { + Logger(`STORAGE -x> DB:${prefixedFileName}: (config) already deleted`); + return; + } + saveData = + { + ...old, + mtime, + size: 0, + children: [], + deleted: true, + type: "newnote", + }; + } + await this.localDatabase.putRaw(saveData); + Logger(`STORAGE -x> DB:${prefixedFileName}: (config) Done`); + } catch (ex) { + Logger(`STORAGE -x> DB:${prefixedFileName}: (config) Failed`); + Logger(ex, LOG_LEVEL.VERBOSE); + return false; + } + }); + } + + async scanInternalFiles(): Promise { + const filenames = (await this.getFiles(this.app.vault.configDir, 2)).filter(e => e.startsWith(".")).filter(e => !e.startsWith(".trash")); + return filenames as FilePath[]; + } + + + + async getFiles( + path: string, + lastDepth: number + ) { + if (lastDepth == -1) return []; + const w = await this.app.vault.adapter.list(path); + let files = [ + ...w.files + ]; + for (const v of w.folders) { + files = files.concat(await this.getFiles(v, lastDepth - 1)); + } + return files; + } +} diff --git a/src/CmdHiddenFileSync.ts b/src/CmdHiddenFileSync.ts index a359f8b..7819f40 100644 --- a/src/CmdHiddenFileSync.ts +++ b/src/CmdHiddenFileSync.ts @@ -4,7 +4,7 @@ import { InternalFileInfo, ICHeader, ICHeaderEnd } from "./types"; import { 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, trimPrefix, isIdOfInternalMetadata, PeriodicProcessor } from "./utils"; +import { disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask, isInternalMetadata, PeriodicProcessor } from "./utils"; import { WrappedNotice } from "./lib/src/wrapper"; import { base64ToArrayBuffer, arrayBufferToBase64 } from "./lib/src/strbin"; import { runWithLock } from "./lib/src/lock"; @@ -28,7 +28,7 @@ export class HiddenFileSync extends LiveSyncCommands { onunload() { this.periodicInternalFileScanProcessor?.disable(); } - onload(): void | Promise { + onload() { this.plugin.addCommand({ id: "livesync-scaninternal", name: "Sync hidden files", @@ -78,7 +78,7 @@ export class HiddenFileSync extends LiveSyncCommands { procInternalFiles: string[] = []; async execInternalFile() { - await runWithLock("execinternal", false, async () => { + await runWithLock("execInternal", false, async () => { const w = [...this.procInternalFiles]; this.procInternalFiles = []; Logger(`Applying hidden ${w.length} files change...`); @@ -95,6 +95,7 @@ export class HiddenFileSync extends LiveSyncCommands { recentProcessedInternalFiles = [] as string[]; async watchVaultRawEventsAsync(path: FilePath) { + if (!this.settings.syncInternalFiles) return; const stat = await this.app.vault.adapter.stat(path); // sometimes folder is coming. if (stat && stat.type != "file") @@ -122,12 +123,6 @@ export class HiddenFileSync extends LiveSyncCommands { await this.deleteInternalFileOnDatabase(path); } else { await this.storeInternalFileToDatabase({ path: path, ...stat }); - const pluginDir = this.app.vault.configDir + "/plugins/"; - const pluginFiles = ["manifest.json", "data.json", "style.css", "main.js"]; - if (path.startsWith(pluginDir) && pluginFiles.some(e => path.endsWith(e)) && this.settings.usePluginSync) { - const pluginName = trimPrefix(path, pluginDir).split("/")[0]; - await this.plugin.addOnPluginAndTheirSettings.sweepPlugin(false, pluginName); - } } } @@ -138,7 +133,7 @@ export class HiddenFileSync extends LiveSyncCommands { for await (const doc of conflicted) { if (!("_conflicts" in doc)) continue; - if (isIdOfInternalMetadata(doc._id)) { + if (isInternalMetadata(doc._id)) { await this.resolveConflictOnInternalFile(doc.path); } } diff --git a/src/CmdPluginAndTheirSettings.ts b/src/CmdPluginAndTheirSettings.ts index b159231..0655eb8 100644 --- a/src/CmdPluginAndTheirSettings.ts +++ b/src/CmdPluginAndTheirSettings.ts @@ -42,6 +42,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands { this.showPluginSyncModal(); }, }); + this.showPluginSyncModal(); } onunload() { this.hidePluginSyncModal(); @@ -165,7 +166,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands { await runWithLock("sweepplugin", true, async () => { const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO; if (!this.deviceAndVaultName) { - Logger("You have to set your device and vault name.", LOG_LEVEL.NOTICE); + Logger("You have to set your device name.", LOG_LEVEL.NOTICE); return; } Logger("Scanning plugins", logLevel); diff --git a/src/CmdSetupLiveSync.ts b/src/CmdSetupLiveSync.ts index 3e87331..4bcfdc5 100644 --- a/src/CmdSetupLiveSync.ts +++ b/src/CmdSetupLiveSync.ts @@ -7,6 +7,7 @@ import { decrypt, encrypt } from "./lib/src/e2ee_v2"; import { LiveSyncCommands } from "./LiveSyncCommands"; import { delay } from "./lib/src/utils"; import { confirmWithMessage } from "./dialogs"; +import { Platform } from "./deps"; export class SetupLiveSync extends LiveSyncCommands { onunload() { } @@ -191,13 +192,14 @@ export class SetupLiveSync extends LiveSyncCommands { } async askHiddenFileConfiguration(opt: { enableFetch?: boolean, enableOverwrite?: boolean }) { this.plugin.addOnSetup.suspendExtraSync(); - const message = `Would you like to enable \`Hidden File Synchronization\`? -${opt.enableFetch ? " - Fetch: Use files stored from other devices. \n" : ""}${opt.enableOverwrite ? "- Overwrite: Use files from this device. \n" : ""}- Keep it disabled: Do not use hidden file synchronization. - -Of course, we are able to disable this feature.` + const message = `Would you like to enable \`Hidden File Synchronization\` or \`Customization sync\`? +${opt.enableFetch ? " - Fetch: Use files stored from other devices. \n" : ""}${opt.enableOverwrite ? "- Overwrite: Use files from this device. \n" : ""}- Custom: Synchronize only customization files with a dedicated interface. +- Keep them disabled: Do not use hidden file synchronization. +Of course, we are able to disable these features.` const CHOICE_FETCH = "Fetch"; const CHOICE_OVERWRITE = "Overwrite"; - const CHOICE_DISMISS = "keep it disabled"; + const CHOICE_CUSTOMIZE = "Custom"; + const CHOICE_DISMISS = "keep them disabled"; const choices = []; if (opt?.enableFetch) { choices.push(CHOICE_FETCH); @@ -205,6 +207,7 @@ Of course, we are able to disable this feature.` if (opt?.enableOverwrite) { choices.push(CHOICE_OVERWRITE); } + choices.push(CHOICE_CUSTOMIZE); choices.push(CHOICE_DISMISS); const ret = await confirmWithMessage(this.plugin, "Hidden file sync", message, choices, CHOICE_DISMISS, 40); @@ -214,26 +217,61 @@ Of course, we are able to disable this feature.` await this.configureHiddenFileSync("OVERWRITE"); } else if (ret == CHOICE_DISMISS) { await this.configureHiddenFileSync("DISABLE"); + } else if (ret == CHOICE_CUSTOMIZE) { + await this.configureHiddenFileSync("CUSTOMIZE"); } } - async configureHiddenFileSync(mode: "FETCH" | "OVERWRITE" | "MERGE" | "DISABLE") { + async configureHiddenFileSync(mode: "FETCH" | "OVERWRITE" | "MERGE" | "DISABLE" | "CUSTOMIZE") { this.plugin.addOnSetup.suspendExtraSync(); if (mode == "DISABLE") { this.plugin.settings.syncInternalFiles = false; + this.plugin.settings.usePluginSync = false; await this.plugin.saveSettings(); return; } - Logger("Gathering files for enabling Hidden File Sync", LOG_LEVEL.NOTICE); - if (mode == "FETCH") { - await this.plugin.addOnHiddenFileSync.syncInternalFilesAndDatabase("pullForce", true); - } else if (mode == "OVERWRITE") { - await this.plugin.addOnHiddenFileSync.syncInternalFilesAndDatabase("pushForce", true); - } else if (mode == "MERGE") { - await this.plugin.addOnHiddenFileSync.syncInternalFilesAndDatabase("safe", true); + if (mode != "CUSTOMIZE") { + Logger("Gathering files for enabling Hidden File Sync", LOG_LEVEL.NOTICE); + if (mode == "FETCH") { + await this.plugin.addOnHiddenFileSync.syncInternalFilesAndDatabase("pullForce", true); + } else if (mode == "OVERWRITE") { + await this.plugin.addOnHiddenFileSync.syncInternalFilesAndDatabase("pushForce", true); + } else if (mode == "MERGE") { + await this.plugin.addOnHiddenFileSync.syncInternalFilesAndDatabase("safe", true); + } + this.plugin.settings.syncInternalFiles = true; + await this.plugin.saveSettings(); + Logger(`Done! Restarting the app is strongly recommended!`, LOG_LEVEL.NOTICE); + } else if (mode == "CUSTOMIZE") { + if (!this.plugin.deviceAndVaultName) { + let name = await askString(this.app, "Device name", "Please set this device name", `desktop`); + if (!name) { + if (Platform.isAndroidApp) { + name = "android-app" + } else if (Platform.isIosApp) { + name = "ios" + } else if (Platform.isMacOS) { + name = "macos" + } else if (Platform.isMobileApp) { + name = "mobile-app" + } else if (Platform.isMobile) { + name = "mobile" + } else if (Platform.isSafari) { + name = "safari" + } else if (Platform.isDesktop) { + name = "desktop" + } else if (Platform.isDesktopApp) { + name = "desktop-app" + } else { + name = "unknown" + } + name = name + Math.random().toString(36).slice(-4); + } + this.plugin.deviceAndVaultName = name; + } + this.plugin.settings.usePluginSync = true; + await this.plugin.saveSettings(); + await this.plugin.addOnConfigSync.scanAllConfigFiles(true); } - this.plugin.settings.syncInternalFiles = true; - await this.plugin.saveSettings(); - Logger(`Done! Restarting the app is strongly recommended!`, LOG_LEVEL.NOTICE); } diff --git a/src/JsonResolveModal.ts b/src/JsonResolveModal.ts index 37b8fbb..c8cc372 100644 --- a/src/JsonResolveModal.ts +++ b/src/JsonResolveModal.ts @@ -8,12 +8,18 @@ export class JsonResolveModal extends Modal { callback: (keepRev: string, mergedStr?: string) => Promise; docs: LoadedEntry[]; component: JsonResolvePane; + nameA: string; + nameB: string; + defaultSelect: string; - constructor(app: App, filename: FilePath, docs: LoadedEntry[], callback: (keepRev: string, mergedStr?: string) => Promise) { + constructor(app: App, filename: FilePath, docs: LoadedEntry[], callback: (keepRev: string, mergedStr?: string) => Promise, nameA?: string, nameB?: string, defaultSelect?: string) { super(app); this.callback = callback; this.filename = filename; this.docs = docs; + this.nameA = nameA; + this.nameB = nameB; + this.defaultSelect = defaultSelect; } async UICallback(keepRev: string, mergedStr?: string) { this.close(); @@ -32,6 +38,9 @@ export class JsonResolveModal extends Modal { props: { docs: this.docs, filename: this.filename, + nameA: this.nameA, + nameB: this.nameB, + defaultSelect: this.defaultSelect, callback: (keepRev, mergedStr) => this.UICallback(keepRev, mergedStr), }, }); diff --git a/src/JsonResolvePane.svelte b/src/JsonResolvePane.svelte index 70dda44..cc25b06 100644 --- a/src/JsonResolvePane.svelte +++ b/src/JsonResolvePane.svelte @@ -10,7 +10,9 @@ Promise.resolve(); }; export let filename: FilePath = "" as FilePath; - + export let nameA: string = "A"; + export let nameB: string = "B"; + export let defaultSelect: string = ""; let docA: LoadedEntry = undefined; let docB: LoadedEntry = undefined; let docAContent = ""; @@ -20,14 +22,8 @@ let objAB: any = {}; let objBA: any = {}; let diffs: Diff[]; - const modes = [ - ["", "Not now"], - ["A", "A"], - ["B", "B"], - ["AB", "A + B"], - ["BA", "B + A"], - ] as ["" | "A" | "B" | "AB" | "BA", string][]; - let mode: "" | "A" | "B" | "AB" | "BA" = ""; + type SelectModes = "" | "A" | "B" | "AB" | "BA"; + let mode: SelectModes = defaultSelect as SelectModes; function docToString(doc: LoadedEntry) { return doc.datatype == "plain" ? getDocData(doc.data) : base64ToString(doc.data); @@ -47,8 +43,13 @@ return getDiff(JSON.stringify(a, null, 2), JSON.stringify(b, null, 2)); } function apply() { - if (mode == "A") return callback(docA._rev, null); - if (mode == "B") return callback(docB._rev, null); + if (docA._id == docB._id) { + if (mode == "A") return callback(docA._rev, null); + if (mode == "B") return callback(docB._rev, null); + } else { + if (mode == "A") return callback(null, docToString(docA)); + if (mode == "B") return callback(null, docToString(docB)); + } if (mode == "BA") return callback(null, JSON.stringify(objBA, null, 2)); if (mode == "AB") return callback(null, JSON.stringify(objAB, null, 2)); callback(null, null); @@ -92,12 +93,19 @@ $: selectedObj = mode in mergedObjs ? mergedObjs[mode] : {}; $: { diffs = getJsonDiff(objA, selectedObj); - console.dir(selectedObj); } + + $: modes = [ + ["", "Not now"], + ["A", nameA || "A"], + ["B", nameB || "B"], + ["AB", `${nameA || "A"} + ${nameB || "B"}`], + ["BA", `${nameB || "B"} + ${nameA || "A"}`], + ] as ["" | "A" | "B" | "AB" | "BA", string][];

Conflicted settings

-
{filename}
+

{filename}

{#if !docA || !docB}
Just for a minute, please!
@@ -125,12 +133,14 @@ NO PREVIEW {/if}
- A Rev:{revStringToRevNumber(docA._rev)} ,{new Date(docA.mtime).toLocaleString()} + {nameA} + {#if docA._id == docB._id} Rev:{revStringToRevNumber(docA._rev)} {/if} ,{new Date(docA.mtime).toLocaleString()} {docAContent.length} letters
- B Rev:{revStringToRevNumber(docB._rev)} ,{new Date(docB.mtime).toLocaleString()} + {nameB} + {#if docA._id == docB._id} Rev:{revStringToRevNumber(docB._rev)} {/if} ,{new Date(docB.mtime).toLocaleString()} {docBContent.length} letters
diff --git a/src/ObsidianLiveSyncSettingTab.ts b/src/ObsidianLiveSyncSettingTab.ts index 2d6e793..68cf21d 100644 --- a/src/ObsidianLiveSyncSettingTab.ts +++ b/src/ObsidianLiveSyncSettingTab.ts @@ -1606,13 +1606,13 @@ ${stringifyYaml(pluginConfig)}`; // With great respect, thank you TfTHacker! // Refer: https://github.com/TfTHacker/obsidian42-brat/blob/main/src/features/BetaPlugins.ts const containerPluginSettings = containerEl.createDiv(); - containerPluginSettings.createEl("h3", { text: "Plugins and settings (beta)" }); + containerPluginSettings.createEl("h3", { text: "Customization sync (beta)" }); const updateDisabledOfDeviceAndVaultName = () => { vaultName.setDisabled(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic); vaultName.setTooltip(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic ? "You could not change when you enabling auto scan." : ""); }; - new Setting(containerPluginSettings).setName("Enable plugin synchronization").addToggle((toggle) => + new Setting(containerPluginSettings).setName("Enable customization sync").addToggle((toggle) => toggle.setValue(this.plugin.settings.usePluginSync).onChange(async (value) => { this.plugin.settings.usePluginSync = value; await this.plugin.saveSettings(); @@ -1620,8 +1620,8 @@ ${stringifyYaml(pluginConfig)}`; ); new Setting(containerPluginSettings) - .setName("Scan plugins automatically") - .setDesc("Scan plugins before replicating.") + .setName("Scan customization automatically") + .setDesc("Scan customization before replicating.") .addToggle((toggle) => toggle.setValue(this.plugin.settings.autoSweepPlugins).onChange(async (value) => { this.plugin.settings.autoSweepPlugins = value; @@ -1631,8 +1631,8 @@ ${stringifyYaml(pluginConfig)}`; ); new Setting(containerPluginSettings) - .setName("Scan plugins periodically") - .setDesc("Scan plugins every 1 minute. This configuration will be ignored if monitoring changes of hidden files has been enabled.") + .setName("Scan customization periodically") + .setDesc("Scan customization every 1 minute. This configuration will be ignored if monitoring changes of hidden files has been enabled.") .addToggle((toggle) => toggle.setValue(this.plugin.settings.autoSweepPluginsPeriodic).onChange(async (value) => { this.plugin.settings.autoSweepPluginsPeriodic = value; @@ -1642,8 +1642,8 @@ ${stringifyYaml(pluginConfig)}`; ); new Setting(containerPluginSettings) - .setName("Notify updates") - .setDesc("Notify when any device has a newer plugin or its setting.") + .setName("Notify customized") + .setDesc("Notify when other device has newly customized.") .addToggle((toggle) => toggle.setValue(this.plugin.settings.notifyPluginOrSettingUpdated).onChange(async (value) => { this.plugin.settings.notifyPluginOrSettingUpdated = value; @@ -1651,10 +1651,10 @@ ${stringifyYaml(pluginConfig)}`; }) ); const vaultName = new Setting(containerPluginSettings) - .setName("Device and Vault name") + .setName("Device name") .setDesc("") .addText((text) => { - text.setPlaceholder("desktop-main") + text.setPlaceholder("desktop") .setValue(this.plugin.deviceAndVaultName) .onChange(async (value) => { this.plugin.deviceAndVaultName = value; @@ -1664,13 +1664,13 @@ ${stringifyYaml(pluginConfig)}`; }); new Setting(containerPluginSettings) .setName("Open") - .setDesc("Open the plugin dialog") + .setDesc("Open the dialog") .addButton((button) => { button .setButtonText("Open") .setDisabled(false) .onClick(() => { - this.plugin.addOnPluginAndTheirSettings.showPluginSyncModal(); + this.plugin.addOnConfigSync.showPluginSyncModal(); }); }); diff --git a/src/PluginCombo.svelte b/src/PluginCombo.svelte new file mode 100644 index 0000000..ab7927f --- /dev/null +++ b/src/PluginCombo.svelte @@ -0,0 +1,314 @@ + + +{#if terms.length > 0} + + {#if !hidden} + + {freshness} + {equivalency} + {version} + + + {#if canApply || (isMaintenanceMode && selected != "")} + {#if canCompare} + + {:else} + + {:else} + + {:else} + + {/if} + {/if} + {/if} +{:else} + + All devices are even + +
+
+ +
-
- - + {#if loading} +
+ Updating list... +
+ {/if} +
+ {#if list.length == 0} +
No Items.
+ {:else} + {#each Object.entries(displays) as [key, label]} +
+

{label}

+ {#each groupBy(filterList(list, [key]), "name") as [name, listX]} +
+
+ {name} +
+
+ {/each} +
+ {/each} +
+

Plugins

+ {#each groupBy(filterList(list, ["PLUGIN_MAIN", "PLUGIN_DATA", "PLUGIN_ETC"]), "name") as [name, listX]} +
+
+ {name} +
+
+
+
Main
+
+
+
Data
+
+ {/each} +
+ {/if}
- -
- - - +
+ +
+
+
- - -
diff --git a/src/StorageEventManager.ts b/src/StorageEventManager.ts index ebcf9cd..cef04d4 100644 --- a/src/StorageEventManager.ts +++ b/src/StorageEventManager.ts @@ -64,7 +64,7 @@ export class StorageEventManagerObsidian extends StorageEventManager { } // Watch raw events (Internal API) watchVaultRawEvents(path: FilePath) { - if (!this.plugin.settings.syncInternalFiles) return; + if (!this.plugin.settings.syncInternalFiles && !this.plugin.settings.usePluginSync) return; if (!this.plugin.settings.watchInternalFileChanges) return; if (!path.startsWith(app.vault.configDir)) return; const ignorePatterns = this.plugin.settings.syncInternalFilesIgnorePatterns diff --git a/src/deps.ts b/src/deps.ts index 2e28dcd..c4ef71f 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -3,6 +3,7 @@ import { FilePath } from "./lib/src/types"; export { addIcon, App, DataWriteOptions, debounce, Editor, FuzzySuggestModal, MarkdownRenderer, MarkdownView, Modal, Notice, Platform, Plugin, PluginManifest, PluginSettingTab, Plugin_2, requestUrl, RequestUrlParam, RequestUrlResponse, sanitizeHTMLToDom, Setting, stringifyYaml, TAbstractFile, TextAreaComponent, TFile, TFolder, + parseYaml } from "obsidian"; import { normalizePath as normalizePath_ diff --git a/src/dialogs.ts b/src/dialogs.ts index c9c12e8..15a1384 100644 --- a/src/dialogs.ts +++ b/src/dialogs.ts @@ -9,6 +9,9 @@ export class PluginDialogModal extends Modal { plugin: ObsidianLiveSyncPlugin; logEl: HTMLDivElement; component: PluginPane = null; + isOpened() { + return this.component != null; + } constructor(app: App, plugin: ObsidianLiveSyncPlugin) { super(app); @@ -223,4 +226,4 @@ export function confirmWithMessage(plugin: Plugin, title: string, contentMd: str const dialog = new MessageBox(plugin, title, contentMd, buttons, defaultAction, timeout, (result) => res(result)); dialog.open(); }); -}; +} diff --git a/src/lib b/src/lib index c14ab28..75f24a2 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit c14ab28b4d4843db4ba9768d8f7e60c102ef7e53 +Subproject commit 75f24a27b0e6a4d47d094d65a98f145da9e17520 diff --git a/src/main.ts b/src/main.ts index 9828a61..8f58837 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,7 +11,7 @@ import { LogDisplayModal } from "./LogDisplayModal"; 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, isIdOfInternalMetadata, isPluginMetadata, stripInternalMetadataPrefix, isChunk, askSelectString, askYesNo, askString, PeriodicProcessor, clearTouched, getPath, getPathWithoutPrefix, getPathFromTFile, localDatabaseCleanUp, balanceChunks, 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, localDatabaseCleanUp, balanceChunks, performRebuildDB } from "./utils"; import { encrypt, tryDecrypt } from "./lib/src/e2ee_v2"; import { enableEncryption, isCloudantURI, isErrorOfMissingDoc, isValidRemoteCouchDBURI } from "./lib/src/utils_couchdb"; import { getGlobalStore, ObservableStore, observeStores } from "./lib/src/store"; @@ -26,9 +26,9 @@ import { LiveSyncLocalDB, LiveSyncLocalDBEnv } from "./lib/src/LiveSyncLocalDB"; import { LiveSyncDBReplicator, LiveSyncReplicatorEnv } from "./lib/src/LiveSyncReplicator"; import { KeyValueDatabase, OpenKeyValueDatabase } from "./KeyValueDB"; import { LiveSyncCommands } from "./LiveSyncCommands"; -import { PluginAndTheirSettings } from "./CmdPluginAndTheirSettings"; import { HiddenFileSync } from "./CmdHiddenFileSync"; import { SetupLiveSync } from "./CmdSetupLiveSync"; +import { ConfigSync } from "./CmdConfigSync"; import { confirmWithMessage } from "./dialogs"; setNoticeClass(Notice); @@ -48,10 +48,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin packageVersion = ""; manifestVersion = ""; - addOnPluginAndTheirSettings = new PluginAndTheirSettings(this); + // addOnPluginAndTheirSettings = new PluginAndTheirSettings(this); addOnHiddenFileSync = new HiddenFileSync(this); addOnSetup = new SetupLiveSync(this); - addOns = [this.addOnPluginAndTheirSettings, this.addOnHiddenFileSync, this.addOnSetup] as LiveSyncCommands[]; + addOnConfigSync = new ConfigSync(this); + addOns = [this.addOnHiddenFileSync, this.addOnSetup, this.addOnConfigSync] as LiveSyncCommands[]; periodicSyncProcessor = new PeriodicProcessor(this, async () => await this.replicate()); @@ -206,7 +207,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin id2path(id: DocumentID, entry: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix { const tempId = id2path(id, entry); - if (stripPrefix && isIdOfInternalMetadata(tempId)) { + if (stripPrefix && isInternalMetadata(tempId)) { const out = stripInternalMetadataPrefix(tempId); return out; } @@ -342,7 +343,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin } async resolveConflicted(target: FilePathWithPrefix) { - if (isIdOfInternalMetadata(target)) { + if (isInternalMetadata(target)) { await this.addOnHiddenFileSync.resolveConflictOnInternalFile(target); } else if (isPluginMetadata(target)) { await this.resolveConflictByNewerEntry(target); @@ -906,6 +907,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin await this.kvDB.set(keyD2, mtime); } else if (queue.type == "INTERNAL") { await this.addOnHiddenFileSync.watchVaultRawEventsAsync(file.path); + await this.addOnConfigSync.watchVaultRawEventsAsync(file.path); } else { const targetFile = this.app.vault.getAbstractFileByPath(file.path); if (!(targetFile instanceof TFile)) { @@ -1283,7 +1285,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin const now = new Date().getTime(); if (queue.missingChildren.length == 0) { queue.done = true; - if (isIdOfInternalMetadata(queue.entry._id)) { + if (isInternalMetadata(queue.entry._id)) { //system file const filename = this.getPathWithoutPrefix(queue.entry); this.addOnHiddenFileSync.procInternalFile(filename); @@ -1328,7 +1330,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin if (!this.isTargetFile(path)) return; const skipOldFile = this.settings.skipOlderFilesOnSync && false; //patched temporary. // Do not handle internal files if the feature has not been enabled. - if (isIdOfInternalMetadata(doc._id) && !this.settings.syncInternalFiles) return; + if (isInternalMetadata(doc._id) && !this.settings.syncInternalFiles) return; // It is better for your own safety, not to handle the following files const ignoreFiles = [ "_design/replicate", @@ -1336,11 +1338,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin FLAGMD_REDFLAG2, FLAGMD_REDFLAG3 ]; - if (!isIdOfInternalMetadata(doc._id) && ignoreFiles.contains(path)) { + if (!isInternalMetadata(doc._id) && ignoreFiles.contains(path)) { return; } - if ((!isIdOfInternalMetadata(doc._id)) && skipOldFile) { + if ((!isInternalMetadata(doc._id)) && skipOldFile) { const info = getAbstractFileByPath(stripAllPrefixes(path)); if (info && info instanceof TFile) { @@ -2227,7 +2229,7 @@ Or if you are sure know what had been happened, we can unlock the database from const dK = `${file.path}-diff`; const isLastDiff = dK in caches ? caches[dK] : { storageMtime: 0, docMtime: 0 }; if (isLastDiff.docMtime == docMtime && isLastDiff.storageMtime == storageMtime) { - Logger("STORAGE .. DB :" + file.path, LOG_LEVEL.VERBOSE); + // Logger("STORAGE .. DB :" + file.path, LOG_LEVEL.VERBOSE); caches[dK] = { storageMtime, docMtime }; return caches; } diff --git a/src/types.ts b/src/types.ts index 7edb921..686b3b9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -62,13 +62,21 @@ export type FileEventItem = { key: string, } +// Hidden items (Now means `chunk`) export const CHeader = "h:"; + +// Plug-in Stored Container (Obsolete) export const PSCHeader = "ps:"; export const PSCHeaderEnd = "ps;"; + +// Internal data Container export const ICHeader = "i:"; export const ICHeaderEnd = "i;"; export const ICHeaderLength = ICHeader.length; +// Internal data Container (eXtended) +export const ICXHeader = "ix:"; + export const FileWatchEventQueueMax = 10; export const configURIBase = "obsidian://setuplivesync?settings="; diff --git a/src/utils.ts b/src/utils.ts index 4520d8c..4eb1186 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -234,8 +234,8 @@ export function applyPatch(from: Record, patch: R } export function mergeObject( - objA: Record, - objB: Record + objA: Record | [any], + objB: Record | [any] ) { const newEntries = Object.entries(objB); const ret: any = { ...objA }; @@ -278,6 +278,11 @@ export function mergeObject( ret[key] = v; } } + if (Array.isArray(objA) && Array.isArray(objB)) { + return Object.values(Object.entries(ret) + .sort() + .reduce((p, [key, value]) => ({ ...p, [key]: value }), {})); + } return Object.entries(ret) .sort() .reduce((p, [key, value]) => ({ ...p, [key]: value }), {}); @@ -362,7 +367,7 @@ export function clearTouched() { * @param id ID * @returns */ -export function isIdOfInternalMetadata(id: FilePath | FilePathWithPrefix | DocumentID): boolean { +export function isInternalMetadata(id: FilePath | FilePathWithPrefix | DocumentID): boolean { return id.startsWith(ICHeader); } export function stripInternalMetadataPrefix(id: T): T {