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,14 +1,14 @@
import { writable } from 'svelte/store'; 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 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 { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "./types";
import { delay, getDocData } from "./lib/src/utils"; import { delay, getDocData } from "./lib/src/utils";
import { Logger } from "./lib/src/logger"; import { Logger } from "./lib/src/logger";
import { WrappedNotice } from "./lib/src/wrapper"; import { WrappedNotice } from "./lib/src/wrapper";
import { base64ToArrayBuffer, arrayBufferToBase64, readString, crc32CKHash } from "./lib/src/strbin"; 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 { LiveSyncCommands } from "./LiveSyncCommands";
import { stripAllPrefixes } from "./lib/src/path"; import { stripAllPrefixes } from "./lib/src/path";
import { PeriodicProcessor, askYesNo, disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "./utils"; import { PeriodicProcessor, askYesNo, disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "./utils";
@@ -16,10 +16,20 @@ import { PluginDialogModal } from "./dialogs";
import { JsonResolveModal } from "./JsonResolveModal"; import { JsonResolveModal } from "./JsonResolveModal";
import { pipeGeneratorToGenerator, processAllGeneratorTasksWithConcurrencyLimit } from './lib/src/task'; import { pipeGeneratorToGenerator, processAllGeneratorTasksWithConcurrencyLimit } from './lib/src/task';
function serialize(data: PluginDataEx): string {
function serialize<T>(obj: T): string { // To improve performance, make JSON manually.
return JSON.stringify(obj, null, 1); // 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<T>(str: string, def: T) { function deserialize<T>(str: string, def: T) {
try { try {
return JSON.parse(str) as T; 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" | "" { 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 == 2 && filePath.endsWith(".json")) return "CONFIG";
if (filePath.split("/").length == 4 && filePath.startsWith(`${this.app.vault.configDir}/themes/`)) return "THEME"; 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) pluginList.set(this.pluginList)
await this.updatePluginList(showMessage); await this.updatePluginList(showMessage);
} }
async loadPluginData(path: FilePathWithPrefix): Promise<PluginDataExDisplay | false> {
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<void> { async updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise<void> {
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO; const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
// pluginList.set([]); // pluginList.set([]);
@@ -174,7 +225,7 @@ export class ConfigSync extends LiveSyncCommands {
} }
await Promise.resolve(); // Just to prevent warning. await Promise.resolve(); // Just to prevent warning.
scheduleTask("update-plugin-list-task", 200, async () => { scheduleTask("update-plugin-list-task", 200, async () => {
await runWithLock("update-plugin-list", false, async () => { await serialized("update-plugin-list", async () => {
try { try {
const updatedDocumentId = updatedDocumentPath ? await this.path2id(updatedDocumentPath) : ""; const updatedDocumentId = updatedDocumentPath ? await this.path2id(updatedDocumentPath) : "";
const plugins = updatedDocumentPath ? const plugins = updatedDocumentPath ?
@@ -193,22 +244,7 @@ export class ConfigSync extends LiveSyncCommands {
count++; count++;
if (count % 10 == 0) Logger(`Enumerating files... ${count}`, logLevel, "get-plugins"); if (count % 10 == 0) Logger(`Enumerating files... ${count}`, logLevel, "get-plugins");
Logger(`plugin-${path}`, LOG_LEVEL_VERBOSE); Logger(`plugin-${path}`, LOG_LEVEL_VERBOSE);
const wx = await this.localDatabase.getDBEntry(path, null, false, false); return this.loadPluginData(path);
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 entries; // return entries;
} catch (ex) { } catch (ex) {
//TODO //TODO
@@ -218,7 +254,7 @@ export class ConfigSync extends LiveSyncCommands {
return false; return false;
}))) { }))) {
if ("ok" in v) { if ("ok" in v) {
if (v.ok != false) { if (v.ok !== false) {
let newList = [...this.pluginList]; let newList = [...this.pluginList];
const item = v.ok; const item = v.ok;
newList = newList.filter(x => x.documentPath != item.documentPath); newList = newList.filter(x => x.documentPath != item.documentPath);
@@ -230,6 +266,7 @@ export class ConfigSync extends LiveSyncCommands {
} }
} }
Logger(`All files enumerated`, logLevel, "get-plugins"); Logger(`All files enumerated`, logLevel, "get-plugins");
this.createMissingConfigurationEntry();
} finally { } finally {
pluginIsEnumerating.set(false); 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 fileA = { ...pluginDataA.files[0], ctime: pluginDataA.files[0].mtime, _id: `${pluginDataA.documentPath}` as DocumentID };
const fileB = pluginDataB.files[0]; const fileB = pluginDataB.files[0];
const docAx = { ...docA, ...fileA } as LoadedEntry, docBx = { ...docB, ...fileB } as LoadedEntry 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); Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE);
// const docs = [docA, docB]; // const docs = [docA, docB];
const path = stripAllPrefixes(docAx.path.split("/").slice(-1).join("/") as FilePath); 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); this.periodicPluginSweepProcessor.enable(this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges ? (PERIODIC_PLUGIN_SWEEP * 1000) : 0);
return; return;
} }
recentProcessedInternalFiles = [] as string[]; recentProcessedInternalFiles = [] as string[];
async makeEntryFromFile(path: FilePath): Promise<false | PluginDataExFile> { async makeEntryFromFile(path: FilePath): Promise<false | PluginDataExFile> {
const stat = await this.app.vault.adapter.stat(path); const stat = await this.app.vault.adapter.stat(path);
@@ -470,7 +508,7 @@ export class ConfigSync extends LiveSyncCommands {
return; return;
} }
const vf = this.filenameToUnifiedKey(path, term); const vf = this.filenameToUnifiedKey(path, term);
return await runWithLock(`plugin-${vf}`, false, async () => { return await serialized(`plugin-${vf}`, async () => {
const category = this.getFileCategory(path); const category = this.getFileCategory(path);
let mtime = 0; let mtime = 0;
let fileTargets = [] as FilePath[]; let fileTargets = [] as FilePath[];
@@ -578,6 +616,13 @@ export class ConfigSync extends LiveSyncCommands {
// Make sure that target is a file. // Make sure that target is a file.
if (stat && stat.type != "file") if (stat && stat.type != "file")
return false; 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 storageMTime = ~~((stat && stat.mtime || 0) / 1000);
const key = `${path}-${storageMTime}`; const key = `${path}-${storageMTime}`;
if (this.recentProcessedInternalFiles.contains(key)) { if (this.recentProcessedInternalFiles.contains(key)) {
@@ -618,7 +663,7 @@ export class ConfigSync extends LiveSyncCommands {
// const id = await this.path2id(prefixedFileName); // const id = await this.path2id(prefixedFileName);
const mtime = new Date().getTime(); const mtime = new Date().getTime();
await runWithLock("file-x-" + prefixedFileName, false, async () => { await serialized("file-x-" + prefixedFileName, async () => {
try { try {
const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, null, false) as InternalFileEntry | false; const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, null, false) as InternalFileEntry | false;
let saveData: InternalFileEntry; let saveData: InternalFileEntry;

View File

@@ -1,13 +1,13 @@
import { Notice, normalizePath, type PluginManifest } from "./deps"; 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 } from "./lib/src/types"; 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 { type InternalFileInfo, ICHeader, ICHeaderEnd } from "./types";
import { Parallels, delay, isDocContentSame } from "./lib/src/utils"; import { Parallels, delay, isDocContentSame } from "./lib/src/utils";
import { Logger } from "./lib/src/logger"; import { Logger } from "./lib/src/logger";
import { PouchDB } from "./lib/src/pouchdb-browser.js"; 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 { WrappedNotice } from "./lib/src/wrapper";
import { base64ToArrayBuffer, arrayBufferToBase64 } from "./lib/src/strbin"; import { base64ToArrayBuffer, arrayBufferToBase64 } from "./lib/src/strbin";
import { runWithLock } from "./lib/src/lock"; import { serialized } from "./lib/src/lock";
import { JsonResolveModal } from "./JsonResolveModal"; import { JsonResolveModal } from "./JsonResolveModal";
import { LiveSyncCommands } from "./LiveSyncCommands"; import { LiveSyncCommands } from "./LiveSyncCommands";
import { addPrefix, stripAllPrefixes } from "./lib/src/path"; import { addPrefix, stripAllPrefixes } from "./lib/src/path";
@@ -77,7 +77,7 @@ export class HiddenFileSync extends LiveSyncCommands {
procInternalFiles: string[] = []; procInternalFiles: string[] = [];
async execInternalFile() { async execInternalFile() {
await runWithLock("execInternal", false, async () => { await serialized("execInternal", async () => {
const w = [...this.procInternalFiles]; const w = [...this.procInternalFiles];
this.procInternalFiles = []; this.procInternalFiles = [];
Logger(`Applying hidden ${w.length} files change...`); Logger(`Applying hidden ${w.length} files change...`);
@@ -95,6 +95,14 @@ export class HiddenFileSync extends LiveSyncCommands {
recentProcessedInternalFiles = [] as string[]; recentProcessedInternalFiles = [] as string[];
async watchVaultRawEventsAsync(path: FilePath) { async watchVaultRawEventsAsync(path: FilePath) {
if (!this.settings.syncInternalFiles) return; 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); const stat = await this.app.vault.adapter.stat(path);
// sometimes folder is coming. // sometimes folder is coming.
if (stat && stat.type != "file") 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... //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(); await this.resolveConflictOnInternalFiles();
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO; const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
Logger("Scanning hidden files.", logLevel, "sync_internal"); Logger("Scanning hidden files.", logLevel, "sync_internal");
const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns
.replace(/\n| /g, "") .replace(/\n| /g, "")
.split(",").filter(e => e).map(e => new RegExp(e, "i")); .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 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 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) { function compareMTime(a: number, b: number) {
const wa = ~~(a / 1000); const wa = ~~(a / 1000);
const wb = ~~(b / 1000); const wb = ~~(b / 1000);
@@ -274,7 +288,7 @@ export class HiddenFileSync extends LiveSyncCommands {
if (ignorePatterns.some(e => filename.match(e))) if (ignorePatterns.some(e => filename.match(e)))
continue; continue;
if (await this.plugin.isIgnoredByIgnoreFiles(filename)) { if (await this.plugin.isIgnoredByIgnoreFiles(filename)) {
continue continue;
} }
const fileOnStorage = filename in filesMap ? filesMap[filename] : undefined; 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. // When files has been retrieved from the database. they must be reloaded.
if ((direction == "pull" || direction == "pullForce") && filesChanged != 0) { 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. // Show notification to restart obsidian when something has been changed in configDir.
if (configDir in updatedFolders) { if (configDir in updatedFolders) {
// Numbers of updated files that is below of configDir. // Numbers of updated files that is below of configDir.
@@ -352,10 +365,7 @@ export class HiddenFileSync extends LiveSyncCommands {
updatedCount -= updatedFolders[manifest.dir]; updatedCount -= updatedFolders[manifest.dir];
const updatePluginId = manifest.id; const updatePluginId = manifest.id;
const updatePluginName = manifest.name; const updatePluginName = manifest.name;
const fragment = createFragment((doc) => { this.plugin.askInPopup(`updated-${updatePluginId}`, `Files in ${updatePluginName} has been updated, Press {HERE} to reload ${updatePluginName}, or press elsewhere to dismiss this message.`, (anchor) => {
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.text = "HERE";
anchor.addEventListener("click", async () => { anchor.addEventListener("click", async () => {
Logger(`Unloading plugin: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId); Logger(`Unloading plugin: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId);
@@ -365,31 +375,8 @@ export class HiddenFileSync extends LiveSyncCommands {
await this.app.plugins.loadPlugin(updatePluginId); await this.app.plugins.loadPlugin(updatePluginId);
Logger(`Plugin reloaded: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId); Logger(`Plugin reloaded: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId);
}); });
}));
a.appendText(` to reload ${updatePluginName}, or press elsewhere to dismiss this message.`);
});
});
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<Notice>(updatedPluginKey);
if (!popup)
return;
//@ts-ignore
if (popup?.noticeEl?.isShown()) {
popup.hide();
}
disposeMemoObject(updatedPluginKey);
});
});
} }
} }
} catch (ex) { } catch (ex) {
@@ -400,31 +387,12 @@ export class HiddenFileSync extends LiveSyncCommands {
// If something changes left, notify for reloading Obsidian. // If something changes left, notify for reloading Obsidian.
if (updatedCount != 0) { if (updatedCount != 0) {
const fragment = createFragment((doc) => { this.plugin.askInPopup(`updated-any-hidden`, `Hidden files have been synchronized, Press {HERE} to reload Obsidian, or press elsewhere to dismiss this message.`, (anchor) => {
doc.createEl("span", null, (a) => {
a.appendText(`Hidden files have been synchronized, Press `);
a.appendChild(a.createEl("a", null, (anchor) => {
anchor.text = "HERE"; anchor.text = "HERE";
anchor.addEventListener("click", () => { anchor.addEventListener("click", () => {
// @ts-ignore // @ts-ignore
this.app.commands.executeCommandById("app:reload"); 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;
});
}); });
} }
} }
@@ -437,6 +405,7 @@ export class HiddenFileSync extends LiveSyncCommands {
if (await this.plugin.isIgnoredByIgnoreFiles(file.path)) { if (await this.plugin.isIgnoredByIgnoreFiles(file.path)) {
return return
} }
const id = await this.path2id(file.path, ICHeader); const id = await this.path2id(file.path, ICHeader);
const prefixedFileName = addPrefix(file.path, ICHeader); const prefixedFileName = addPrefix(file.path, ICHeader);
const contentBin = await this.app.vault.adapter.readBinary(file.path); const contentBin = await this.app.vault.adapter.readBinary(file.path);
@@ -449,7 +418,7 @@ export class HiddenFileSync extends LiveSyncCommands {
return false; return false;
} }
const mtime = file.mtime; const mtime = file.mtime;
return await runWithLock("file-" + prefixedFileName, false, async () => { return await serialized("file-" + prefixedFileName, async () => {
try { try {
const old = await this.localDatabase.getDBEntry(prefixedFileName, null, false, false); const old = await this.localDatabase.getDBEntry(prefixedFileName, null, false, false);
let saveData: LoadedEntry; let saveData: LoadedEntry;
@@ -501,7 +470,7 @@ export class HiddenFileSync extends LiveSyncCommands {
if (await this.plugin.isIgnoredByIgnoreFiles(filename)) { if (await this.plugin.isIgnoredByIgnoreFiles(filename)) {
return return
} }
await runWithLock("file-" + prefixedFileName, false, async () => { await serialized("file-" + prefixedFileName, async () => {
try { try {
const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, null, true) as InternalFileEntry | false; const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, null, true) as InternalFileEntry | false;
let saveData: InternalFileEntry; let saveData: InternalFileEntry;
@@ -547,7 +516,7 @@ export class HiddenFileSync extends LiveSyncCommands {
if (await this.plugin.isIgnoredByIgnoreFiles(filename)) { if (await this.plugin.isIgnoredByIgnoreFiles(filename)) {
return; return;
} }
return await runWithLock("file-" + prefixedFileName, false, async () => { return await serialized("file-" + prefixedFileName, async () => {
try { try {
// Check conflicted status // Check conflicted status
//TODO option //TODO option
@@ -618,7 +587,7 @@ export class HiddenFileSync extends LiveSyncCommands {
showJSONMergeDialogAndMerge(docA: LoadedEntry, docB: LoadedEntry): Promise<boolean> { showJSONMergeDialogAndMerge(docA: LoadedEntry, docB: LoadedEntry): Promise<boolean> {
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); Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE);
const docs = [docA, docB]; const docs = [docA, docB];
const path = stripAllPrefixes(docA.path); const path = stripAllPrefixes(docA.path);
@@ -676,13 +645,16 @@ export class HiddenFileSync extends LiveSyncCommands {
} }
async scanInternalFiles(): Promise<InternalFileInfo[]> { async scanInternalFiles(): Promise<InternalFileInfo[]> {
const configDir = normalizePath(this.app.vault.configDir);
const ignoreFilter = this.settings.syncInternalFilesIgnorePatterns const ignoreFilter = this.settings.syncInternalFilesIgnorePatterns
.replace(/\n| /g, "") .replace(/\n| /g, "")
.split(",").filter(e => e).map(e => new RegExp(e, "i")); .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 root = this.app.vault.getRoot();
const findRoot = root.path; const findRoot = root.path;
const filenames = (await this.getFiles(findRoot, [], null, ignoreFilter)).filter(e => e.startsWith(".")).filter(e => !e.startsWith(".trash")); 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 { return {
path: e as FilePath, path: e as FilePath,
stat: await this.app.vault.adapter.stat(e) stat: await this.app.vault.adapter.stat(e)
@@ -716,7 +688,7 @@ export class HiddenFileSync extends LiveSyncCommands {
...w.files ...w.files
.filter((e) => !ignoreList.some((ee) => e.endsWith(ee))) .filter((e) => !ignoreList.some((ee) => e.endsWith(ee)))
.filter((e) => !filter || filter.some((ee) => e.match(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[]; let files = [] as string[];
for (const file of filesSrc) { for (const file of filesSrc) {

View File

@@ -9,7 +9,7 @@ import { isPluginMetadata, PeriodicProcessor } from "./utils";
import { PluginDialogModal } from "./dialogs"; import { PluginDialogModal } from "./dialogs";
import { NewNotice } from "./lib/src/wrapper"; import { NewNotice } from "./lib/src/wrapper";
import { versionNumberString2Number } from "./lib/src/strbin"; import { versionNumberString2Number } from "./lib/src/strbin";
import { runWithLock } from "./lib/src/lock"; import { serialized, skipIfDuplicated } from "./lib/src/lock";
import { LiveSyncCommands } from "./LiveSyncCommands"; import { LiveSyncCommands } from "./LiveSyncCommands";
export class PluginAndTheirSettings extends LiveSyncCommands { export class PluginAndTheirSettings extends LiveSyncCommands {
@@ -164,7 +164,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
if (specificPluginPath != "") { if (specificPluginPath != "") {
specificPlugin = manifests.find(e => e.dir.endsWith("/" + specificPluginPath))?.id ?? ""; 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; const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
if (!this.deviceAndVaultName) { if (!this.deviceAndVaultName) {
Logger("You have to set your device name.", LOG_LEVEL_NOTICE); Logger("You have to set your device name.", LOG_LEVEL_NOTICE);
@@ -223,7 +223,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
type: "plain" type: "plain"
}; };
Logger(`check diff:${m.name}(${m.id})`, LOG_LEVEL_VERBOSE); 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); const old = await this.localDatabase.getDBEntry(p._id as string as FilePathWithPrefix /* This also should be explained */, null, false, false);
if (old !== false) { if (old !== false) {
const oldData = { data: old.data, deleted: old._deleted }; const oldData = { data: old.data, deleted: old._deleted };
@@ -266,7 +266,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
} }
async applyPluginData(plugin: PluginDataEntry) { 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 pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/";
const adapter = this.app.vault.adapter; const adapter = this.app.vault.adapter;
// @ts-ignore // @ts-ignore
@@ -288,7 +288,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
} }
async applyPlugin(plugin: PluginDataEntry) { async applyPlugin(plugin: PluginDataEntry) {
await runWithLock("plugin-" + plugin.manifest.id, false, async () => { await serialized("plugin-" + plugin.manifest.id, async () => {
// @ts-ignore // @ts-ignore
const stat = this.app.plugins.enabledPlugins.has(plugin.manifest.id) == true; const stat = this.app.plugins.enabledPlugins.has(plugin.manifest.id) == true;
if (stat) { if (stat) {

View File

@@ -20,6 +20,11 @@ export class SetupLiveSync extends LiveSyncCommands {
name: "Copy the setup URI", name: "Copy the setup URI",
callback: this.command_copySetupURI.bind(this), 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({ this.plugin.addCommand({
id: "livesync-copysetupurifull", id: "livesync-copysetupurifull",
@@ -41,11 +46,14 @@ export class SetupLiveSync extends LiveSyncCommands {
} }
async realizeSettingSyncMode() { } 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); const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "The passphrase to encrypt the setup URI", "", true);
if (encryptingPassphrase === false) if (encryptingPassphrase === false)
return; return;
const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" }; const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" };
if (stripExtra) {
delete setting.pluginSyncExtendedSetting;
}
const keys = Object.keys(setting) as (keyof ObsidianLiveSyncSettings)[]; const keys = Object.keys(setting) as (keyof ObsidianLiveSyncSettings)[];
for (const k of keys) { for (const k of keys) {
if (JSON.stringify(k in setting ? setting[k] : "") == JSON.stringify(k in DEFAULT_SETTINGS ? DEFAULT_SETTINGS[k] : "*")) { 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); await navigator.clipboard.writeText(uri);
Logger("Setup URI copied to clipboard", LOG_LEVEL_NOTICE); Logger("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
} }
async command_copySetupURIWithSync() {
this.command_copySetupURI(false);
}
async command_openSetupURI() { async command_openSetupURI() {
const setupURI = await askString(this.app, "Easy setup", "Set up URI", `${configURIBase}aaaaa`); const setupURI = await askString(this.app, "Easy setup", "Set up URI", `${configURIBase}aaaaa`);
if (setupURI === false) if (setupURI === false)
@@ -290,6 +301,7 @@ Of course, we are able to disable these features.`
this.plugin.settings.liveSync = false; this.plugin.settings.liveSync = false;
this.plugin.settings.periodicReplication = false; this.plugin.settings.periodicReplication = false;
this.plugin.settings.syncOnSave = false; this.plugin.settings.syncOnSave = false;
this.plugin.settings.syncOnEditorSave = false;
this.plugin.settings.syncOnStart = false; this.plugin.settings.syncOnStart = false;
this.plugin.settings.syncOnFileOpen = false; this.plugin.settings.syncOnFileOpen = false;
this.plugin.settings.syncAfterMerge = false; this.plugin.settings.syncAfterMerge = false;

View File

@@ -18,10 +18,8 @@ export class ConflictResolveModal extends Modal {
onOpen() { onOpen() {
const { contentEl } = this; const { contentEl } = this;
this.titleEl.setText("Conflicting changes");
contentEl.empty(); contentEl.empty();
contentEl.createEl("h2", { text: "This document has conflicted changes." });
contentEl.createEl("span", { text: this.filename }); contentEl.createEl("span", { text: this.filename });
const div = contentEl.createDiv(""); const div = contentEl.createDiv("");
div.addClass("op-scrollable"); div.addClass("op-scrollable");

View File

@@ -10,29 +10,29 @@ import { stripPrefix } from "./lib/src/path";
export class DocumentHistoryModal extends Modal { export class DocumentHistoryModal extends Modal {
plugin: ObsidianLiveSyncPlugin; plugin: ObsidianLiveSyncPlugin;
range: HTMLInputElement; range!: HTMLInputElement;
contentView: HTMLDivElement; contentView!: HTMLDivElement;
info: HTMLDivElement; info!: HTMLDivElement;
fileInfo: HTMLDivElement; fileInfo!: HTMLDivElement;
showDiff = false; showDiff = false;
id: DocumentID; id?: DocumentID;
file: FilePathWithPrefix; file: FilePathWithPrefix;
revs_info: PouchDB.Core.RevisionInfo[] = []; revs_info: PouchDB.Core.RevisionInfo[] = [];
currentDoc: LoadedEntry; currentDoc?: LoadedEntry;
currentText = ""; currentText = "";
currentDeleted = false; 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); super(app);
this.plugin = plugin; this.plugin = plugin;
this.file = (file instanceof TFile) ? getPathFromTFile(file) : file; this.file = (file instanceof TFile) ? getPathFromTFile(file) : file;
this.id = id; this.id = id;
this.initialRev = revision; this.initialRev = revision;
if (!file) { if (!file && id) {
this.file = this.plugin.id2path(id, null); this.file = this.plugin.id2path(id);
} }
if (localStorage.getItem("ols-history-highlightdiff") == "1") { if (localStorage.getItem("ols-history-highlightdiff") == "1") {
this.showDiff = true; this.showDiff = true;
@@ -46,8 +46,8 @@ export class DocumentHistoryModal extends Modal {
const db = this.plugin.localDatabase; const db = this.plugin.localDatabase;
try { try {
const w = await db.localDatabase.get(this.id, { revs_info: true }); const w = await db.localDatabase.get(this.id, { revs_info: true });
this.revs_info = w._revs_info.filter((e) => e?.status == "available"); this.revs_info = w._revs_info?.filter((e) => e?.status == "available") ?? [];
this.range.max = `${this.revs_info.length - 1}`; this.range.max = `${Math.max(this.revs_info.length - 1, 0)}`;
this.range.value = this.range.max; this.range.value = this.range.max;
this.fileInfo.setText(`${this.file} / ${this.revs_info.length} revisions`); this.fileInfo.setText(`${this.file} / ${this.revs_info.length} revisions`);
await this.loadRevs(initialRev); await this.loadRevs(initialRev);
@@ -90,7 +90,7 @@ export class DocumentHistoryModal extends Modal {
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`; this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
let result = ""; let result = "";
const w1data = w.datatype == "plain" ? getDocData(w.data) : base64ToString(w.data); const w1data = w.datatype == "plain" ? getDocData(w.data) : base64ToString(w.data);
this.currentDeleted = w.deleted; this.currentDeleted = !!w.deleted;
this.currentText = w1data; this.currentText = w1data;
if (this.showDiff) { if (this.showDiff) {
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1); const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
@@ -130,9 +130,8 @@ export class DocumentHistoryModal extends Modal {
onOpen() { onOpen() {
const { contentEl } = this; const { contentEl } = this;
this.titleEl.setText("Document History");
contentEl.empty(); contentEl.empty();
contentEl.createEl("h2", { text: "Document History" });
this.fileInfo = contentEl.createDiv(""); this.fileInfo = contentEl.createDiv("");
this.fileInfo.addClass("op-info"); this.fileInfo.addClass("op-info");
const divView = contentEl.createDiv(""); const divView = contentEl.createDiv("");

View File

@@ -29,7 +29,7 @@ export class JsonResolveModal extends Modal {
onOpen() { onOpen() {
const { contentEl } = this; const { contentEl } = this;
this.titleEl.setText("Conflicted Setting");
contentEl.empty(); contentEl.empty();
if (this.component == null) { if (this.component == null) {

View File

@@ -104,7 +104,6 @@
] as ["" | "A" | "B" | "AB" | "BA", string][]; ] as ["" | "A" | "B" | "AB" | "BA", string][];
</script> </script>
<h1>Conflicted settings</h1>
<h2>{filename}</h2> <h2>{filename}</h2>
{#if !docA || !docB} {#if !docA || !docB}
<div class="message">Just for a minute, please!</div> <div class="message">Just for a minute, please!</div>

View File

@@ -14,9 +14,9 @@ export class LogDisplayModal extends Modal {
onOpen() { onOpen() {
const { contentEl } = this; const { contentEl } = this;
this.titleEl.setText("Sync status");
contentEl.empty(); contentEl.empty();
contentEl.createEl("h2", { text: "Sync Status" });
const div = contentEl.createDiv(""); const div = contentEl.createDiv("");
div.addClass("op-scrollable"); div.addClass("op-scrollable");
div.addClass("op-pre"); div.addClass("op-pre");

View File

@@ -120,6 +120,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
if (this.plugin.settings.periodicReplication) return true; if (this.plugin.settings.periodicReplication) return true;
if (this.plugin.settings.syncOnFileOpen) return true; if (this.plugin.settings.syncOnFileOpen) return true;
if (this.plugin.settings.syncOnSave) 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.syncOnStart) return true;
if (this.plugin.settings.syncAfterMerge) return true; if (this.plugin.settings.syncAfterMerge) return true;
if (this.plugin.replicator.syncStatus == "CONNECTED") 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.liveSync = false;
this.plugin.settings.periodicReplication = false; this.plugin.settings.periodicReplication = false;
this.plugin.settings.syncOnSave = false; this.plugin.settings.syncOnSave = false;
this.plugin.settings.syncOnEditorSave = false;
this.plugin.settings.syncOnStart = false; this.plugin.settings.syncOnStart = false;
this.plugin.settings.syncOnFileOpen = false; this.plugin.settings.syncOnFileOpen = false;
this.plugin.settings.syncAfterMerge = false; this.plugin.settings.syncAfterMerge = false;
@@ -216,7 +218,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
syncLive.forEach((e) => { syncLive.forEach((e) => {
e.setDisabled(false).setTooltip(""); 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) => { syncNonLive.forEach((e) => {
e.setDisabled(false).setTooltip(""); e.setDisabled(false).setTooltip("");
}); });
@@ -891,6 +893,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
liveSync: false, liveSync: false,
periodicReplication: false, periodicReplication: false,
syncOnSave: false, syncOnSave: false,
syncOnEditorSave: false,
syncOnStart: false, syncOnStart: false,
syncOnFileOpen: false, syncOnFileOpen: false,
syncAfterMerge: false, syncAfterMerge: false,
@@ -904,6 +907,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
batchSave: true, batchSave: true,
periodicReplication: true, periodicReplication: true,
syncOnSave: false, syncOnSave: false,
syncOnEditorSave: false,
syncOnStart: true, syncOnStart: true,
syncOnFileOpen: true, syncOnFileOpen: true,
syncAfterMerge: true, syncAfterMerge: true,
@@ -1014,6 +1018,17 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
applyDisplayEnabled(); 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) new Setting(containerSyncSettingEl)
.setName("Sync on File Open") .setName("Sync on File Open")
.setDesc("When you open file, sync automatically") .setDesc("When you open file, sync automatically")
@@ -1199,7 +1214,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, \\/obsidian-livesync\\/"; const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, \\/obsidian-livesync\\/";
const defaultSkipPatternXPlat = defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$"; const defaultSkipPatternXPlat = defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$";
new Setting(containerSyncSettingEl) new Setting(containerSyncSettingEl)
.setName("Skip patterns") .setName("Folders and files to ignore")
.setDesc( .setDesc(
"Regular expression, If you use hidden file sync between desktop and mobile, adding `workspace$` is recommended." "Regular expression, If you use hidden file sync between desktop and mobile, adding `workspace$` is recommended."
) )
@@ -1777,8 +1792,8 @@ ${stringifyYaml(pluginConfig)}`;
dropdown dropdown
.addOptions({ "": "Old Algorithm", "xxhash32": "xxhash32 (Fast)", "xxhash64": "xxhash64 (Fastest)" } as Record<HashAlgorithm, string>) .addOptions({ "": "Old Algorithm", "xxhash32": "xxhash32 (Fast)", "xxhash64": "xxhash64 (Fastest)" } as Record<HashAlgorithm, string>)
.setValue(this.plugin.settings.hashAlg) .setValue(this.plugin.settings.hashAlg)
.onChange(async (value: HashAlgorithm) => { .onChange(async (value) => {
this.plugin.settings.hashAlg = value; this.plugin.settings.hashAlg = value as HashAlgorithm;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
) )

View File

@@ -108,7 +108,7 @@
} }
} }
}) })
.reduce((p, c) => p | c, 0); .reduce((p, c) => p | (c as number), 0 as number);
if (matchingStatus == 0b0000100) { if (matchingStatus == 0b0000100) {
equivalency = "⚖️ Same"; equivalency = "⚖️ Same";
canApply = false; canApply = false;

View File

@@ -3,9 +3,13 @@
import ObsidianLiveSyncPlugin from "./main"; import ObsidianLiveSyncPlugin from "./main";
import { type PluginDataExDisplay, pluginIsEnumerating, pluginList } from "./CmdConfigSync"; import { type PluginDataExDisplay, pluginIsEnumerating, pluginList } from "./CmdConfigSync";
import PluginCombo from "./PluginCombo.svelte"; 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; export let plugin: ObsidianLiveSyncPlugin;
$: hideNotApplicable = true; $: hideNotApplicable = false;
$: thisTerm = plugin.deviceAndVaultName; $: thisTerm = plugin.deviceAndVaultName;
const addOn = plugin.addOnConfigSync; const addOn = plugin.addOnConfigSync;
@@ -13,7 +17,7 @@
let list: PluginDataExDisplay[] = []; let list: PluginDataExDisplay[] = [];
let selectNewestPulse = 0; let selectNewestPulse = 0;
let hideEven = true; let hideEven = false;
let loading = false; let loading = false;
let applyAllPluse = 0; let applyAllPluse = 0;
let isMaintenanceMode = false; let isMaintenanceMode = false;
@@ -80,6 +84,54 @@
async function deleteData(data: PluginDataExDisplay): Promise<boolean> { async function deleteData(data: PluginDataExDisplay): Promise<boolean> {
return await addOn.deleteData(data); 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 = { $: options = {
thisTerm, thisTerm,
@@ -92,11 +144,84 @@
plugin, plugin,
isMaintenanceMode, 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<string, SYNC_MODE>();
let automaticListDisp = new Map<string, SYNC_MODE>();
// 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<string, string[]> = {};
$: {
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<string, string[]>);
}
</script> </script>
<div> <div>
<div> <div>
<h1>Customization sync</h1>
<div class="buttons"> <div class="buttons">
<button on:click={() => scanAgain()}>Scan changes</button> <button on:click={() => scanAgain()}>Scan changes</button>
<button on:click={() => replicate()}>Sync once</button> <button on:click={() => replicate()}>Sync once</button>
@@ -119,15 +244,24 @@
{#if list.length == 0} {#if list.length == 0}
<div class="center">No Items.</div> <div class="center">No Items.</div>
{:else} {:else}
{#each Object.entries(displays) as [key, label]} {#each Object.entries(displays).filter(([key, _]) => key in displayKeys) as [key, label]}
<div> <div>
<h3>{label}</h3> <h3>{label}</h3>
{#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}
<div class="labelrow {hideEven ? 'hideeven' : ''}"> <div class="labelrow {hideEven ? 'hideeven' : ''}">
<div class="title"> <div class="title">
{name} <button class="status" on:click={(evt) => askMode(evt, `${key}/${name}`, bindKey)}>
{getIcon(mode)}
</button>
<span class="name">{name}</span>
</div> </div>
<PluginCombo {...options} list={listX} hidden={false} /> {#if mode == MODE_SELECTIVE}
<PluginCombo {...options} list={list.filter((e) => e.category == key && e.name == name)} hidden={false} />
{:else}
<div class="statusnote">{TITLES[mode]}</div>
{/if}
</div> </div>
{/each} {/each}
</div> </div>
@@ -135,20 +269,55 @@
<div> <div>
<h3>Plugins</h3> <h3>Plugins</h3>
{#each groupBy(filterList(list, ["PLUGIN_MAIN", "PLUGIN_DATA", "PLUGIN_ETC"]), "name") as [name, listX]} {#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}
<div class="labelrow {hideEven ? 'hideeven' : ''}"> <div class="labelrow {hideEven ? 'hideeven' : ''}">
<div class="title"> <div class="title">
{name} <button class="status" on:click={(evt) => askMode(evt, `${PREFIX_PLUGIN_ALL}/${name}`, bindKeyAll)}>
{getIcon(modeAll)}
</button>
<span class="name">{name}</span>
</div> </div>
{#if modeAll == MODE_SELECTIVE}
<PluginCombo {...options} list={listX} hidden={true} /> <PluginCombo {...options} list={listX} hidden={true} />
{/if}
</div> </div>
{#if modeAll == MODE_SELECTIVE}
<div class="filerow {hideEven ? 'hideeven' : ''}"> <div class="filerow {hideEven ? 'hideeven' : ''}">
<div class="filetitle">Main</div> <div class="filetitle">
<button class="status" on:click={(evt) => askMode(evt, `${PREFIX_PLUGIN_MAIN}/${name}/MAIN`, bindKeyMain)}>
{getIcon(modeMain)}
</button>
<span class="name">MAIN</span>
</div>
{#if modeMain == MODE_SELECTIVE}
<PluginCombo {...options} list={filterList(listX, ["PLUGIN_MAIN"])} hidden={false} /> <PluginCombo {...options} list={filterList(listX, ["PLUGIN_MAIN"])} hidden={false} />
{:else}
<div class="statusnote">{TITLES[modeMain]}</div>
{/if}
</div> </div>
<div class="filerow {hideEven ? 'hideeven' : ''}"> <div class="filerow {hideEven ? 'hideeven' : ''}">
<div class="filetitle">Data</div> <div class="filetitle">
<PluginCombo {...options} list={filterList(listX, ["PLUGIN_DATA"])} hidden={false} /> <button class="status" on:click={(evt) => askMode(evt, `${PREFIX_PLUGIN_DATA}/${name}`, bindKeyData)}>
{getIcon(modeData)}
</button>
<span class="name">DATA</span>
</div> </div>
{#if modeData == MODE_SELECTIVE}
<PluginCombo {...options} list={filterList(listX, ["PLUGIN_DATA"])} hidden={false} />
{:else}
<div class="statusnote">{TITLES[modeData]}</div>
{/if}
</div>
{:else}
<div class="noterow">
<div class="statusnote">{TITLES[modeAll]}</div>
</div>
{/if}
{/each} {/each}
</div> </div>
{/if} {/if}
@@ -162,6 +331,15 @@
</div> </div>
<style> <style>
span.spacer {
min-width: 1px;
flex-grow: 1;
}
h3 {
position: sticky;
top: 0;
background-color: var(--modal-background);
}
.labelrow { .labelrow {
margin-left: 0.4em; margin-left: 0.4em;
display: flex; display: flex;
@@ -183,6 +361,24 @@
.labelrow.hideeven:has(.even) { .labelrow.hideeven:has(.even) {
display: none; display: none;
} }
.noterow {
min-height: 2em;
display: flex;
}
button.status {
flex-grow: 0;
margin: 2px 4px;
min-width: 3em;
max-width: 4em;
}
.statusnote {
display: flex;
justify-content: flex-end;
padding-right: var(--size-4-12);
align-items: center;
min-width: 10em;
flex-grow: 1;
}
.title { .title {
color: var(--text-normal); color: var(--text-normal);

View File

@@ -18,7 +18,7 @@ type LiveSyncForStorageEventManager = Plugin &
ignoreFiles: string[], ignoreFiles: string[],
} & { } & {
isTargetFile: (file: string | TAbstractFile) => Promise<boolean>, isTargetFile: (file: string | TAbstractFile) => Promise<boolean>,
procFileEvent: (applyBatch?: boolean) => Promise<boolean>, procFileEvent: (applyBatch?: boolean) => Promise<any>,
}; };

View File

@@ -4,7 +4,7 @@ export {
addIcon, App, debounce, Editor, FuzzySuggestModal, MarkdownRenderer, MarkdownView, Modal, Notice, Platform, Plugin, PluginSettingTab, requestUrl, sanitizeHTMLToDom, Setting, stringifyYaml, TAbstractFile, TextAreaComponent, TFile, TFolder, addIcon, App, debounce, Editor, FuzzySuggestModal, MarkdownRenderer, MarkdownView, Modal, Notice, Platform, Plugin, PluginSettingTab, requestUrl, sanitizeHTMLToDom, Setting, stringifyYaml, TAbstractFile, TextAreaComponent, TFile, TFolder,
parseYaml, ItemView, WorkspaceLeaf parseYaml, ItemView, WorkspaceLeaf
} from "obsidian"; } from "obsidian";
export type { DataWriteOptions, PluginManifest, RequestUrlParam, RequestUrlResponse } from "obsidian"; export type { DataWriteOptions, PluginManifest, RequestUrlParam, RequestUrlResponse, MarkdownFileInfo } from "obsidian";
import { import {
normalizePath as normalizePath_ normalizePath as normalizePath_
} from "obsidian"; } from "obsidian";

View File

@@ -20,6 +20,7 @@ export class PluginDialogModal extends Modal {
onOpen() { onOpen() {
const { contentEl } = this; const { contentEl } = this;
this.titleEl.setText("Customization Sync (Beta2)")
if (this.component == null) { if (this.component == null) {
this.component = new PluginPane({ this.component = new PluginPane({
target: contentEl, target: contentEl,
@@ -38,7 +39,7 @@ export class PluginDialogModal extends Modal {
export class InputStringDialog extends Modal { export class InputStringDialog extends Modal {
result: string | false = false; result: string | false = false;
onSubmit: (result: string | boolean) => void; onSubmit: (result: string | false) => void;
title: string; title: string;
key: string; key: string;
placeholder: string; placeholder: string;
@@ -56,8 +57,7 @@ export class InputStringDialog extends Modal {
onOpen() { onOpen() {
const { contentEl } = this; const { contentEl } = this;
this.titleEl.setText(this.title);
contentEl.createEl("h1", { text: this.title });
// For enter to submit // For enter to submit
const formEl = contentEl.createEl("form"); const formEl = contentEl.createEl("form");
new Setting(formEl).setName(this.key).setClass(this.isPassword ? "password-input" : "normal-input").addText((text) => new Setting(formEl).setName(this.key).setClass(this.isPassword ? "password-input" : "normal-input").addText((text) =>
@@ -144,7 +144,7 @@ export class MessageBox extends Modal {
timer: ReturnType<typeof setInterval> = undefined; timer: ReturnType<typeof setInterval> = undefined;
defaultButtonComponent: ButtonComponent | undefined; defaultButtonComponent: ButtonComponent | undefined;
onSubmit: (result: string | boolean) => void; onSubmit: (result: string | false) => void;
constructor(plugin: Plugin, title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout: number, onSubmit: (result: (typeof buttons)[number] | false) => void) { constructor(plugin: Plugin, title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout: number, onSubmit: (result: (typeof buttons)[number] | false) => void) {
super(plugin.app); super(plugin.app);
@@ -175,6 +175,7 @@ export class MessageBox extends Modal {
onOpen() { onOpen() {
const { contentEl } = this; const { contentEl } = this;
this.titleEl.setText(this.title);
contentEl.addEventListener("click", () => { contentEl.addEventListener("click", () => {
if (this.timer) { if (this.timer) {
clearInterval(this.timer); clearInterval(this.timer);

Submodule src/lib updated: 70eb916288...6548bd3ed7

View File

@@ -1,7 +1,7 @@
const isDebug = false; const isDebug = false;
import { type Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "./deps"; 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 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 { type InternalFileInfo, type queueItem, type CacheData, type FileEventItem, FileWatchEventQueueMax } from "./types";
import { arrayToChunkedArray, getDocData, isDocContentSame } from "./lib/src/utils"; 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 { ConflictResolveModal } from "./ConflictResolveModal";
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab"; import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
import { DocumentHistoryModal } from "./DocumentHistoryModal"; 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 { encrypt, tryDecrypt } from "./lib/src/e2ee_v2";
import { balanceChunkPurgedDBs, enableEncryption, isCloudantURI, isErrorOfMissingDoc, isValidRemoteCouchDBURI, purgeUnreferencedChunks } from "./lib/src/utils_couchdb"; import { balanceChunkPurgedDBs, enableEncryption, isCloudantURI, isErrorOfMissingDoc, isValidRemoteCouchDBURI, purgeUnreferencedChunks } from "./lib/src/utils_couchdb";
import { getGlobalStore, ObservableStore, observeStores } from "./lib/src/store"; import { getGlobalStore, ObservableStore, observeStores } from "./lib/src/store";
import { lockStore, logMessageStore, logStore, type LogEntry } from "./lib/src/stores"; import { lockStore, logMessageStore, logStore, type LogEntry } from "./lib/src/stores";
import { setNoticeClass } from "./lib/src/wrapper"; 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 { 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 { Semaphore } from "./lib/src/semaphore";
import { StorageEventManager, StorageEventManagerObsidian } from "./StorageEventManager"; import { StorageEventManager, StorageEventManagerObsidian } from "./StorageEventManager";
import { LiveSyncLocalDB, type LiveSyncLocalDBEnv } from "./lib/src/LiveSyncLocalDB"; 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))); 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 export default class ObsidianLiveSyncPlugin extends Plugin
implements LiveSyncLocalDBEnv, LiveSyncReplicatorEnv { implements LiveSyncLocalDBEnv, LiveSyncReplicatorEnv {
settings: ObsidianLiveSyncSettings; settings!: ObsidianLiveSyncSettings;
localDatabase: LiveSyncLocalDB; localDatabase!: LiveSyncLocalDB;
replicator: LiveSyncDBReplicator; replicator!: LiveSyncDBReplicator;
statusBar: HTMLElement; statusBar?: HTMLElement;
suspended: boolean; suspended: boolean = true;
deviceAndVaultName: string; deviceAndVaultName: string = "";
isMobile = false; isMobile = false;
isReady = false; isReady = false;
packageVersion = ""; packageVersion = "";
@@ -67,25 +80,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin
periodicSyncProcessor = new PeriodicProcessor(this, async () => await this.replicate()); periodicSyncProcessor = new PeriodicProcessor(this, async () => await this.replicate());
// implementing interfaces // implementing interfaces
kvDB: KeyValueDatabase; kvDB!: KeyValueDatabase;
last_successful_post = false; last_successful_post = false;
getLastPostFailedBySize() { getLastPostFailedBySize() {
return !this.last_successful_post; return !this.last_successful_post;
} }
async fetchByAPI(request: RequestUrlParam): Promise<RequestUrlResponse> {
const ret = await requestUrl(request); _unloaded = false;
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;
}
getDatabase(): PouchDB.Database<EntryDoc> { getDatabase(): PouchDB.Database<EntryDoc> {
return this.localDatabase.localDatabase; 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."; if (uri.indexOf(" ") !== -1) return "Remote URI and database name could not contain spaces.";
let authHeader = ""; let authHeader = "";
if (auth.username && auth.password) { 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); const encoded = window.btoa(utf8str);
authHeader = "Basic " + encoded; authHeader = "Basic " + encoded;
} else { } else {
@@ -115,11 +118,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin
adapter: "http", adapter: "http",
auth, auth,
skip_setup: !performSetup, skip_setup: !performSetup,
fetch: async (url: string | Request, opts: RequestInit) => { fetch: async (url: string | Request, opts?: RequestInit) => {
let size = ""; let size = "";
const localURL = url.toString().substring(uri.length); const localURL = url.toString().substring(uri.length);
const method = opts.method ?? "GET"; const method = opts?.method ?? "GET";
if (opts.body) { if (opts?.body) {
const opts_length = opts.body.toString().length; const opts_length = opts.body.toString().length;
if (opts_length > 1000 * 1000 * 10) { if (opts_length > 1000 * 1000 * 10) {
// over 10MB // over 10MB
@@ -132,10 +135,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin
size = ` (${opts_length})`; size = ` (${opts_length})`;
} }
if (!disableRequestURI && typeof url == "string" && typeof (opts.body ?? "") == "string") { if (!disableRequestURI && typeof url == "string" && typeof (opts?.body ?? "") == "string") {
const body = opts.body as 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; if (authHeader != "") transformedHeaders["authorization"] = authHeader;
delete transformedHeaders["host"]; delete transformedHeaders["host"];
delete transformedHeaders["Host"]; delete transformedHeaders["Host"];
@@ -143,7 +146,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
delete transformedHeaders["Content-Length"]; delete transformedHeaders["Content-Length"];
const requestParam: RequestUrlParam = { const requestParam: RequestUrlParam = {
url, url,
method: opts.method, method: opts?.method,
body: body, body: body,
headers: transformedHeaders, headers: transformedHeaders,
contentType: "application/json", contentType: "application/json",
@@ -151,7 +154,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
}; };
try { try {
const r = await this.fetchByAPI(requestParam); const r = await fetchByAPI(requestParam);
if (method == "POST" || method == "PUT") { if (method == "POST" || method == "PUT") {
this.last_successful_post = r.status - (r.status % 100) == 200; this.last_successful_post = r.status - (r.status % 100) == 200;
} else { } else {
@@ -209,9 +212,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin
try { try {
const info = await db.info(); const info = await db.info();
return { db: db, info: info }; return { db: db, info: info };
} catch (ex) { } catch (ex: any) {
let msg = `${ex.name}:${ex.message}`; let msg = `${ex?.name}:${ex?.message}`;
if (ex.name == "TypeError" && ex.message == "Failed to fetch") { 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."; 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); 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); const tempId = id2path(id, entry);
if (stripPrefix && isInternalMetadata(tempId)) { if (stripPrefix && isInternalMetadata(tempId)) {
const out = stripInternalMetadataPrefix(tempId); const out = stripInternalMetadataPrefix(tempId);
@@ -234,18 +237,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin
return getPathWithoutPrefix(entry); return getPathWithoutPrefix(entry);
} }
async path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise<DocumentID> { 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 : ""); 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) { if (this.settings.useIndexedDBAdapter) {
options.adapter = "indexeddb"; optionPass.adapter = "indexeddb";
//@ts-ignore :missing def //@ts-ignore :missing def
options.purged_infos_limit = 1; optionPass.purged_infos_limit = 1;
return new PouchDB(name + "-indexeddb", options); return new PouchDB(name + "-indexeddb", optionPass);
} }
return new PouchDB(name, options); return new PouchDB(name, optionPass);
} }
beforeOnUnload(db: LiveSyncLocalDB): void { beforeOnUnload(db: LiveSyncLocalDB): void {
this.kvDB.close(); this.kvDB.close();
@@ -258,6 +262,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
this.replicator = new LiveSyncDBReplicator(this); this.replicator = new LiveSyncDBReplicator(this);
} }
async onResetDatabase(db: LiveSyncLocalDB): Promise<void> { async onResetDatabase(db: LiveSyncLocalDB): Promise<void> {
const lsKey = "obsidian-livesync-queuefiles-" + this.getVaultName();
localStorage.removeItem(lsKey);
await this.kvDB.destroy(); await this.kvDB.destroy();
this.kvDB = await OpenKeyValueDatabase(db.dbname + "-livesync-kv"); this.kvDB = await OpenKeyValueDatabase(db.dbname + "-livesync-kv");
this.replicator = new LiveSyncDBReplicator(this); 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(); 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); const target = await askSelectString(this.app, "File to view History", notesList);
if (target) { if (target) {
const targetId = notes.find(e => e.dispPath == target); const targetId = notes.find(e => e.dispPath == target);
this.showHistory(targetId.path, undefined); this.showHistory(targetId.path, targetId.id);
} }
} }
async pickFileForResolve() { async pickFileForResolve() {
@@ -349,7 +355,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
Logger("There are no conflicted documents", LOG_LEVEL_NOTICE); Logger("There are no conflicted documents", LOG_LEVEL_NOTICE);
return false; 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) { if (target) {
const targetItem = notes.find(e => e.dispPath == target); const targetItem = notes.find(e => e.dispPath == target);
await this.resolveConflicted(targetItem.path); await this.resolveConflicted(targetItem.path);
@@ -363,6 +369,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
await this.addOnHiddenFileSync.resolveConflictOnInternalFile(target); await this.addOnHiddenFileSync.resolveConflictOnInternalFile(target);
} else if (isPluginMetadata(target)) { } else if (isPluginMetadata(target)) {
await this.resolveConflictByNewerEntry(target); await this.resolveConflictByNewerEntry(target);
} else if (isCustomisationSyncMetadata(target)) {
await this.resolveConflictByNewerEntry(target);
} else { } else {
await this.showIfConflicted(target); await this.showIfConflicted(target);
} }
@@ -453,6 +461,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
} }
await this.realizeSettingSyncMode(); await this.realizeSettingSyncMode();
this.registerWatchEvents(); this.registerWatchEvents();
this.swapSaveCommand();
if (this.settings.syncOnStart) { if (this.settings.syncOnStart) {
this.replicator.openReplication(this.settings, false, false); 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 }); notes.push({ path: this.getPath(doc), mtime: doc.mtime });
} }
if (notes.length > 0) { 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) { for (const note of notes) {
Logger(`Conflicted: ${note.path}`); Logger(`Conflicted: ${note.path}`);
} }
@@ -512,6 +529,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
if (last_version && Number(last_version) < VER) { if (last_version && Number(last_version) < VER) {
this.settings.liveSync = false; this.settings.liveSync = false;
this.settings.syncOnSave = false; this.settings.syncOnSave = false;
this.settings.syncOnEditorSave = false;
this.settings.syncOnStart = false; this.settings.syncOnStart = false;
this.settings.syncOnFileOpen = false; this.settings.syncOnFileOpen = false;
this.settings.syncAfterMerge = false; this.settings.syncAfterMerge = false;
@@ -572,15 +590,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin
this.addCommand({ this.addCommand({
id: "livesync-dump", id: "livesync-dump",
name: "Dump information of this doc ", name: "Dump information of this doc ",
editorCallback: (editor: Editor, view: MarkdownView) => { editorCallback: (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
this.localDatabase.getDBEntry(getPathFromTFile(view.file), {}, true, false); const file = view.file;
if (!file) return;
this.localDatabase.getDBEntry(getPathFromTFile(file), {}, true, false);
}, },
}); });
this.addCommand({ this.addCommand({
id: "livesync-checkdoc-conflicted", id: "livesync-checkdoc-conflicted",
name: "Resolve if conflicted.", name: "Resolve if conflicted.",
editorCallback: async (editor: Editor, view: MarkdownView) => { editorCallback: async (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
await this.showIfConflicted(getPathFromTFile(view.file)); const file = view.file;
if (!file) return;
await this.showIfConflicted(getPathFromTFile(file));
}, },
}); });
@@ -617,8 +639,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
this.addCommand({ this.addCommand({
id: "livesync-history", id: "livesync-history",
name: "Show history", name: "Show history",
editorCallback: (editor: Editor, view: MarkdownView) => { editorCallback: (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
this.showHistory(view.file, null); if (view.file) this.showHistory(view.file, null);
}, },
}); });
this.addCommand({ this.addCommand({
@@ -720,6 +742,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
} }
cancelAllPeriodicTask(); cancelAllPeriodicTask();
cancelAllTasks(); cancelAllTasks();
this._unloaded = true;
Logger("unloading plugin"); Logger("unloading plugin");
} }
@@ -852,7 +875,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
(async () => await this.realizeSettingSyncMode())(); (async () => await this.realizeSettingSyncMode())();
} }
async saveSettings() {
async saveSettingData() {
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.getVaultName(); const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.getVaultName();
localStorage.setItem(lsKey, this.deviceAndVaultName || ""); localStorage.setItem(lsKey, this.deviceAndVaultName || "");
@@ -882,6 +906,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin
await this.saveData(settings); await this.saveData(settings);
this.localDatabase.settings = this.settings; this.localDatabase.settings = this.settings;
this.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim()); this.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim());
}
async saveSettings() {
await this.saveSettingData();
this.triggerRealizeSettingSyncMode(); this.triggerRealizeSettingSyncMode();
} }
@@ -889,7 +917,38 @@ export default class ObsidianLiveSyncPlugin extends Plugin
registerFileWatchEvents() { registerFileWatchEvents() {
this.vaultManager = new StorageEventManagerObsidian(this) 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() { registerWatchEvents() {
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen)); this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
this.registerDomEvent(document, "visibilitychange", this.watchWindowVisibility); this.registerDomEvent(document, "visibilitychange", this.watchWindowVisibility);
@@ -949,7 +1008,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
} }
} }
cancelTask("applyBatchAuto"); cancelTask("applyBatchAuto");
const ret = await runWithLock("procFiles", true, async () => { const ret = await skipIfDuplicated("procFiles", async () => {
do { do {
const queue = this.vaultManager.fetchEvent(); const queue = this.vaultManager.fetchEvent();
if (queue === false) break; 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.settings.suspendFileWatching) return;
if (!this.isReady) return; if (!this.isReady) return;
if (!file) return;
this.watchWorkspaceOpenAsync(file); this.watchWorkspaceOpenAsync(file);
} }
@@ -1269,7 +1329,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
const path = getPath(entry); const path = getPath(entry);
try { try {
const releaser = await semaphore.acquire(1); 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); Logger(`Applying ${path} (${entry._id}: ${entry._rev}) change...`, LOG_LEVEL_VERBOSE);
await this.handleDBChangedAsync(entry); await this.handleDBChangedAsync(entry);
Logger(`Applied ${path} (${entry._id}:${entry._rev}) change...`); 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.remoteLockedAndDeviceNotAccepted) {
if (this.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) { if (this.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) {
Logger(`The remote database has been cleaned.`, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO); 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 count = await purgeUnreferencedChunks(this.localDatabase.localDatabase, true);
const message = `The remote database has been cleaned up. 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. 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); Logger(`could not get old revisions, automatically used newer one:${path}`, LOG_LEVEL_NOTICE);
return true; return true;
} }
// first, check for same contents and deletion status.
if (leftLeaf.data == rightLeaf.data && leftLeaf.deleted == rightLeaf.deleted) { const isSame = leftLeaf.data == rightLeaf.data && leftLeaf.deleted == rightLeaf.deleted;
let leaf = leftLeaf; const isBinary = !isPlainText(path);
if (leftLeaf.mtime > rightLeaf.mtime) { const alwaysNewer = this.settings.resolveConflictsByNewerFile;
leaf = rightLeaf; if (isSame || isBinary || alwaysNewer) {
}
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 lMtime = ~~(leftLeaf.mtime / 1000); const lMtime = ~~(leftLeaf.mtime / 1000);
const rMtime = ~~(rightLeaf.mtime / 1000); const rMtime = ~~(rightLeaf.mtime / 1000);
let loser = leftLeaf; 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.localDatabase.deleteDBEntry(path, { rev: loser.rev });
await this.pullFile(path, null, true); 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; return true;
} }
// make diff. // 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> { showMergeDialog(filename: FilePathWithPrefix, conflictCheckResult: diff_result): Promise<boolean> {
return runWithLock("resolve-conflict:" + filename, false, () => return serialized("resolve-conflict:" + filename, () =>
new Promise((res, rej) => { new Promise((res, rej) => {
Logger("open conflict dialog", LOG_LEVEL_VERBOSE); Logger("open conflict dialog", LOG_LEVEL_VERBOSE);
new ConflictResolveModal(this.app, filename, conflictCheckResult, async (selected) => { 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) { async showIfConflicted(filename: FilePathWithPrefix) {
await runWithLock("conflicted", false, async () => { await serialized("conflicted", async () => {
const conflictCheckResult = await this.getConflictedStatus(filename); const conflictCheckResult = await this.getConflictedStatus(filename);
if (conflictCheckResult === false) { if (conflictCheckResult === false) {
//nothing to do. //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 //upsert should locked
const msg = `DB <- STORAGE (${datatype}) `; const msg = `DB <- STORAGE (${datatype}) `;
const isNotChanged = await runWithLock("file-" + fullPath, false, async () => { const isNotChanged = await serialized("file-" + fullPath, async () => {
if (recentlyTouched(file)) { if (recentlyTouched(file)) {
return true; 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); return this.localDatabase.isTargetFile(filepath);
} }
async dryRunGC() { async dryRunGC() {
await runWithLock("cleanup", true, async () => { await skipIfDuplicated("cleanup", async () => {
const remoteDBConn = await this.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.isMobile) const remoteDBConn = await this.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.isMobile)
if (typeof (remoteDBConn) == "string") { if (typeof (remoteDBConn) == "string") {
Logger(remoteDBConn); Logger(remoteDBConn);
@@ -2622,7 +2675,7 @@ Or if you are sure know what had been happened, we can unlock the database from
async dbGC() { async dbGC() {
// Lock the remote completely once. // Lock the remote completely once.
await runWithLock("cleanup", true, async () => { await skipIfDuplicated("cleanup", async () => {
this.getReplicator().markRemoteLocked(this.settings, true, true); this.getReplicator().markRemoteLocked(this.settings, true, true);
const remoteDBConn = await this.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.isMobile) const remoteDBConn = await this.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.isMobile)
if (typeof (remoteDBConn) == "string") { 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.") 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);
});
});
}
} }

View File

@@ -3,9 +3,10 @@ import { path2id_base, id2path_base, isValidFilenameInLinux, isValidFilenameInDa
import { Logger } from "./lib/src/logger"; import { Logger } from "./lib/src/logger";
import { LOG_LEVEL_VERBOSE, type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix } from "./lib/src/types"; import { LOG_LEVEL_VERBOSE, type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix } from "./lib/src/types";
import { CHeader, ICHeader, ICHeaderLength, PSCHeader } from "./types"; import { CHeader, ICHeader, ICHeaderLength, ICXHeader, PSCHeader } from "./types";
import { InputStringDialog, PopoverSelectString } from "./dialogs"; import { InputStringDialog, PopoverSelectString } from "./dialogs";
import ObsidianLiveSyncPlugin from "./main"; import ObsidianLiveSyncPlugin from "./main";
import { writeString } from "./lib/src/strbin";
// For backward compatibility, using the path for determining id. // For backward compatibility, using the path for determining id.
// Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/". // Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/".
@@ -387,6 +388,9 @@ export function isChunk(str: string): boolean {
export function isPluginMetadata(str: string): boolean { export function isPluginMetadata(str: string): boolean {
return str.startsWith(PSCHeader); return str.startsWith(PSCHeader);
} }
export function isCustomisationSyncMetadata(str: string): boolean {
return str.startsWith(ICXHeader);
}
export const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => { export const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => {
return new Promise((res) => { return new Promise((res) => {
@@ -440,7 +444,7 @@ export class PeriodicProcessor {
} }
export const _requestToCouchDBFetch = async (baseUri: string, username: string, password: string, path?: string, body?: string | any, method?: string) => { export const _requestToCouchDBFetch = async (baseUri: string, username: string, password: string, path?: string, body?: string | any, method?: string) => {
const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${username}:${password}`)); const utf8str = String.fromCharCode.apply(null, [...writeString(`${username}:${password}`)]);
const encoded = window.btoa(utf8str); const encoded = window.btoa(utf8str);
const authHeader = "Basic " + encoded; const authHeader = "Basic " + encoded;
const transformedHeaders: Record<string, string> = { authorization: authHeader, "content-type": "application/json" }; const transformedHeaders: Record<string, string> = { authorization: authHeader, "content-type": "application/json" };
@@ -456,7 +460,7 @@ export const _requestToCouchDBFetch = async (baseUri: string, username: string,
} }
export const _requestToCouchDB = async (baseUri: string, username: string, password: string, origin: string, path?: string, body?: any, method?: string) => { export const _requestToCouchDB = async (baseUri: string, username: string, password: string, origin: string, path?: string, body?: any, method?: string) => {
const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${username}:${password}`)); const utf8str = String.fromCharCode.apply(null, [...writeString(`${username}:${password}`)]);
const encoded = window.btoa(utf8str); const encoded = window.btoa(utf8str);
const authHeader = "Basic " + encoded; const authHeader = "Basic " + encoded;
const transformedHeaders: Record<string, string> = { authorization: authHeader, origin: origin }; const transformedHeaders: Record<string, string> = { authorization: authHeader, origin: origin };