1
0
mirror of https://github.com/vrtmrz/obsidian-livesync.git synced 2025-01-05 12:50:41 +02:00

New feature:

- Now we are ready for i18n.
- The setting dialogue has been refined. Very controllable, clearly displayed disabled items, and ready to i18n.
Fixed:
- Many memory leaks have been rescued.
- Chunk caches now work well.
- Many trivial but potential bugs are fixed.
- No longer error messages will be shown on retrieving checkpoint or server information.
- Now we can check and correct tweak mismatch during the setup
Improved:
- Customisation synchronisation has got more smoother.
Tidied
- Practically unused functions have been removed or are being prepared for removal.
- Many of the type-errors and lint errors have been corrected.
- Unused files have been removed.
Note:
- From this version, some test files have been included. However, they are not enabled and released in the release build.
This commit is contained in:
vorotamoroz 2024-05-22 14:04:22 +01:00
parent 7b0ac22c3b
commit b3a85c5462
19 changed files with 2117 additions and 1723 deletions

View File

@ -16,6 +16,7 @@ if you want to view the source, please visit the github repository of this plugi
const prod = process.argv[2] === "production";
const keepTest = !prod;
const terserOpt = {
sourceMap: (!prod ? {
@ -142,16 +143,17 @@ const context = await esbuild.context({
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main_org.js",
mainFields: ["browser", "module", "main"],
minifyWhitespace: false,
minifySyntax: false,
minifyIdentifiers: false,
minify: false,
dropLabels: prod && !keepTest ? ["TEST", "DEV"] : [],
// keepNames: true,
plugins: [
sveltePlugin({
preprocess: sveltePreprocess(),
compilerOptions: { css: true, preserveComments: true },
compilerOptions: { css: "injected", preserveComments: false },
}),
...plugins
],

View File

@ -34,6 +34,7 @@ export class ObsHttpHandler extends FetchHttpHandler {
options === undefined ? undefined : options.requestTimeout;
this.reverseProxyNoSignUrl = reverseProxyNoSignUrl;
}
// eslint-disable-next-line require-await
async handle(
request: HttpRequest,
{ abortSignal }: HttpHandlerOptions = {}

View File

@ -4,10 +4,10 @@ import { Notice, type PluginManifest, parseYaml, normalizePath, type ListedFiles
import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, DocumentID, AnyEntry, SavingEntry } from "../lib/src/common/types.ts";
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE } from "../lib/src/common/types.ts";
import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "../common/types.ts";
import { createSavingEntryFromLoadedEntry, createTextBlob, delay, fireAndForget, getDocData, isDocContentSame, throttle } from "../lib/src/common/utils.ts";
import { createSavingEntryFromLoadedEntry, createTextBlob, delay, fireAndForget, getDocDataAsArray, isDocContentSame, throttle } from "../lib/src/common/utils.ts";
import { Logger } from "../lib/src/common/logger.ts";
import { readString, decodeBinary, arrayBufferToBase64, digestHash } from "../lib/src/string_and_binary/strbin.ts";
import { serialized } from "../lib/src/concurrency/lock.ts";
import { serialized, shareRunningResult } from "../lib/src/concurrency/lock.ts";
import { LiveSyncCommands } from "./LiveSyncCommands.ts";
import { stripAllPrefixes } from "../lib/src/string_and_binary/path.ts";
import { PeriodicProcessor, disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "../common/utils.ts";
@ -19,6 +19,8 @@ import type ObsidianLiveSyncPlugin from '../main.ts';
const d = "\u200b";
const d2 = "\n";
const delimiters = /(?<=[\n|\u200b])/g;
function serialize(data: PluginDataEx): string {
// For higher performance, create custom plug-in data strings.
@ -30,7 +32,7 @@ function serialize(data: PluginDataEx): string {
ret += data.mtime + d2;
for (const file of data.files) {
ret += file.filename + d + (file.displayName ?? "") + d + (file.version ?? "") + d2;
const hash = digestHash((file.data ?? []).join());
const hash = digestHash((file.data ?? []));
ret += file.mtime + d + file.size + d + hash + d2;
for (const data of file.data ?? []) {
ret += data + d
@ -39,41 +41,49 @@ function serialize(data: PluginDataEx): string {
}
return ret;
}
function fetchToken(source: string, from: number): [next: number, token: string] {
const limitIdx = source.indexOf(d2, from);
const limit = limitIdx == -1 ? source.length : limitIdx;
const delimiterIdx = source.indexOf(d, from);
const delimiter = delimiterIdx == -1 ? source.length : delimiterIdx;
const tokenEnd = Math.min(limit, delimiter);
let next = tokenEnd;
if (limit < delimiter) {
next = tokenEnd;
} else {
next = tokenEnd + 1
}
return [next, source.substring(from, tokenEnd)];
}
function getTokenizer(source: string) {
function getTokenizer(source: string[]) {
const sources = source.flatMap(e => e.split(delimiters))
sources[0] = sources[0].substring(1);
let pos = 0;
let lineRunOut = false;
const t = {
pos: 1,
next() {
const [next, token] = fetchToken(source, this.pos);
this.pos = next;
return token;
next(): string {
if (lineRunOut) {
return "";
}
if (pos >= sources.length) {
return "";
}
const item = sources[pos];
if (!item.endsWith(d2)) {
pos++;
} else {
lineRunOut = true;
}
if (item.endsWith(d) || item.endsWith(d2)) {
return item.substring(0, item.length - 1);
} else {
return item + this.next();
}
},
nextLine() {
const nextPos = source.indexOf(d2, this.pos);
if (nextPos == -1) {
this.pos = source.length;
if (lineRunOut) {
pos++;
} else {
this.pos = nextPos + 1;
while (!sources[pos].endsWith(d2)) {
pos++;
if (pos >= sources.length) break;
}
pos++;
}
lineRunOut = false;
}
}
return t;
}
function deserialize2(str: string): PluginDataEx {
function deserialize2(str: string[]): PluginDataEx {
const tokens = getTokenizer(str);
const ret = {} as PluginDataEx;
const category = tokens.next();
@ -120,13 +130,16 @@ function deserialize2(str: string): PluginDataEx {
return result;
}
function deserialize<T>(str: string, def: T) {
function deserialize<T>(str: string[], def: T) {
try {
if (str[0] == ":") return deserialize2(str);
return JSON.parse(str) as T;
if (str[0][0] == ":") {
const o = deserialize2(str);
return o;
}
return JSON.parse(str.join("")) as T;
} catch (ex) {
try {
return parseYaml(str);
return parseYaml(str.join(""));
} catch (ex) {
return def;
}
@ -277,14 +290,14 @@ export class ConfigSync extends LiveSyncCommands {
async loadPluginData(path: FilePathWithPrefix): Promise<PluginDataExDisplay | false> {
const wx = await this.localDatabase.getDBEntry(path, undefined, false, false);
if (wx) {
const data = deserialize(getDocData(wx.data), {}) as PluginDataEx;
const data = deserialize(getDocDataAsArray(wx.data), {}) as PluginDataEx;
const xFiles = [] as PluginDataExFile[];
let missingHash = false;
for (const file of data.files) {
const work = { ...file, data: [] as string[] };
if (!file.hash) {
// debugger;
const tempStr = getDocData(work.data);
const tempStr = getDocDataAsArray(work.data);
const hash = digestHash(tempStr);
file.hash = hash;
missingHash = true;
@ -326,6 +339,7 @@ export class ConfigSync extends LiveSyncCommands {
if (saveRequired) {
this.plugin.saveSettingData();
}
}
pluginScanProcessor = new QueueProcessor(async (v: AnyEntry[]) => {
@ -384,9 +398,9 @@ export class ConfigSync extends LiveSyncCommands {
const docB = await this.localDatabase.getDBEntry(dataB.documentPath);
if (docA && docB) {
const pluginDataA = deserialize(getDocData(docA.data), {}) as PluginDataEx;
const pluginDataA = deserialize(getDocDataAsArray(docA.data), {}) as PluginDataEx;
pluginDataA.documentPath = dataA.documentPath;
const pluginDataB = deserialize(getDocData(docB.data), {}) as PluginDataEx;
const pluginDataB = deserialize(getDocDataAsArray(docB.data), {}) as PluginDataEx;
pluginDataB.documentPath = dataB.documentPath;
// Use outer structure to wrap each data.
@ -425,7 +439,7 @@ export class ConfigSync extends LiveSyncCommands {
if (dx == false) {
throw "Not found on database"
}
const loadedData = deserialize(getDocData(dx.data), {}) as PluginDataEx;
const loadedData = deserialize(getDocDataAsArray(dx.data), {}) as PluginDataEx;
for (const f of loadedData.files) {
Logger(`Applying ${f.filename} of ${data.displayName || data.name}..`);
try {
@ -688,7 +702,7 @@ export class ConfigSync extends LiveSyncCommands {
}
const oldC = await this.localDatabase.getDBEntryFromMeta(old, {}, false, false);
if (oldC) {
const d = await deserialize(getDocData(oldC.data), {}) as PluginDataEx;
const d = await deserialize(getDocDataAsArray(oldC.data), {}) as PluginDataEx;
const diffs = (d.files.map(previous => ({ prev: previous, curr: dt.files.find(e => e.filename == previous.filename) })).map(async e => {
try { return await isDocContentSame(e.curr?.data ?? [], e.prev.data) } catch (_) { return false }
}))
@ -750,31 +764,33 @@ export class ConfigSync extends LiveSyncCommands {
async scanAllConfigFiles(showMessage: boolean) {
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
Logger("Scanning customizing files.", logLevel, "scan-all-config");
const term = this.plugin.deviceAndVaultName;
if (term == "") {
Logger("We have to configure the device name", LOG_LEVEL_NOTICE);
return;
}
const filesAll = await this.scanInternalFiles();
const files = filesAll.filter(e => this.isTargetPath(e)).map(e => ({ key: this.filenameToUnifiedKey(e), file: e }));
const virtualPathsOfLocalFiles = [...new Set(files.map(e => e.key))];
const filesOnDB = ((await this.localDatabase.allDocsRaw({ startkey: ICXHeader + "", endkey: `${ICXHeader}\u{10ffff}`, include_docs: true })).rows.map(e => e.doc) as InternalFileEntry[]).filter(e => !e.deleted);
let deleteCandidate = filesOnDB.map(e => this.getPath(e)).filter(e => e.startsWith(`${ICXHeader}${term}/`));
for (const vp of virtualPathsOfLocalFiles) {
const p = files.find(e => e.key == vp)?.file;
if (!p) {
Logger(`scanAllConfigFiles - File not found: ${vp}`, LOG_LEVEL_VERBOSE);
continue;
await shareRunningResult("scanAllConfigFiles", async () => {
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
Logger("Scanning customizing files.", logLevel, "scan-all-config");
const term = this.plugin.deviceAndVaultName;
if (term == "") {
Logger("We have to configure the device name", LOG_LEVEL_NOTICE);
return;
}
await this.storeCustomizationFiles(p);
deleteCandidate = deleteCandidate.filter(e => e != vp);
}
for (const vp of deleteCandidate) {
await this.deleteConfigOnDatabase(vp);
}
this.updatePluginList(false).then(/* fire and forget */);
const filesAll = await this.scanInternalFiles();
const files = filesAll.filter(e => this.isTargetPath(e)).map(e => ({ key: this.filenameToUnifiedKey(e), file: e }));
const virtualPathsOfLocalFiles = [...new Set(files.map(e => e.key))];
const filesOnDB = ((await this.localDatabase.allDocsRaw({ startkey: ICXHeader + "", endkey: `${ICXHeader}\u{10ffff}`, include_docs: true })).rows.map(e => e.doc) as InternalFileEntry[]).filter(e => !e.deleted);
let deleteCandidate = filesOnDB.map(e => this.getPath(e)).filter(e => e.startsWith(`${ICXHeader}${term}/`));
for (const vp of virtualPathsOfLocalFiles) {
const p = files.find(e => e.key == vp)?.file;
if (!p) {
Logger(`scanAllConfigFiles - File not found: ${vp}`, LOG_LEVEL_VERBOSE);
continue;
}
await this.storeCustomizationFiles(p);
deleteCandidate = deleteCandidate.filter(e => e != vp);
}
for (const vp of deleteCandidate) {
await this.deleteConfigOnDatabase(vp);
}
this.updatePluginList(false).then(/* fire and forget */);
});
}
async deleteConfigOnDatabase(prefixedFileName: FilePathWithPrefix, forceWrite = false) {

View File

@ -144,7 +144,7 @@ export class HiddenFileSync extends LiveSyncCommands {
Logger("something went wrong on resolving all conflicted internal files");
Logger(ex, LOG_LEVEL_VERBOSE);
}
await this.conflictResolutionProcessor.startPipeline().waitForPipeline();
await this.conflictResolutionProcessor.startPipeline().waitForAllProcessed();
}
async resolveByNewerEntry(id: DocumentID, path: FilePathWithPrefix, currentDoc: EntryDoc, currentRev: string, conflictedRev: string) {
@ -388,7 +388,7 @@ export class HiddenFileSync extends LiveSyncCommands {
}, { suspended: true, batchSize: 1, concurrentLimit: 5, delay: 0 }))
.root
.enqueueAll(allFileNames)
.startPipeline().waitForPipeline();
.startPipeline().waitForAllDoneAndTerminate();
await this.kvDB.set("diff-caches-internal", caches);

@ -1 +1 @@
Subproject commit 13f8370ef52682888ebddccfa60b6b66201e49c1
Subproject commit ed85f79cf76e81ae01939c818c28661534c5fe5f

View File

@ -31,7 +31,7 @@ import { GlobalHistoryView, VIEW_TYPE_GLOBAL_HISTORY } from "./ui/GlobalHistoryV
import { LogPaneView, VIEW_TYPE_LOG } from "./ui/LogPaneView.ts";
import { LRUCache } from "./lib/src/memory/LRUCache.ts";
import { SerializedFileAccess } from "./storages/SerializedFileAccess.js";
import { QueueProcessor } from "./lib/src/concurrency/processor.js";
import { QueueProcessor, stopAllRunningProcessors } from "./lib/src/concurrency/processor.js";
import { reactive, reactiveSource, type ReactiveValue } from "./lib/src/dataobject/reactive.js";
import { initializeStores } from "./common/stores.js";
import { JournalSyncMinio } from "./lib/src/replication/journal/objectstore/JournalSyncMinio.js";
@ -39,7 +39,9 @@ import { LiveSyncJournalReplicator, type LiveSyncJournalReplicatorEnv } from "./
import { LiveSyncCouchDBReplicator, type LiveSyncCouchDBReplicatorEnv } from "./lib/src/replication/couchdb/LiveSyncReplicator.js";
import type { CheckPointInfo } from "./lib/src/replication/journal/JournalSyncTypes.js";
import { ObsHttpHandler } from "./common/ObsHttpHandler.js";
// import { Trench } from "./lib/src/memory/memutil.js";
import { TestPaneView, VIEW_TYPE_TEST } from "./tests/TestPaneView.js"
import { $f, __onMissingTranslation, setLang } from "./lib/src/common/i18n.ts";
setNoticeClass(Notice);
@ -85,6 +87,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
settings!: ObsidianLiveSyncSettings;
localDatabase!: LiveSyncLocalDB;
replicator!: LiveSyncAbstractReplicator;
settingTab!: ObsidianLiveSyncSettingTab;
statusBar?: HTMLElement;
_suspended = false;
@ -223,12 +226,17 @@ export default class ObsidianLiveSyncPlugin extends Plugin
}
Logger(`HTTP:${method}${size} to:${localURL} -> ${response.status}`, LOG_LEVEL_DEBUG);
if (Math.floor(response.status / 100) !== 2) {
const r = response.clone();
Logger(`The request may have failed. The reason sent by the server: ${r.status}: ${r.statusText}`);
try {
Logger(await (await r.blob()).text(), LOG_LEVEL_VERBOSE);
} catch (_) {
Logger("Cloud not parse response", LOG_LEVEL_VERBOSE);
if (method != "GET" && localURL.indexOf("/_local/") === -1 && !localURL.endsWith("/")) {
const r = response.clone();
Logger(`The request may have failed. The reason sent by the server: ${r.status}: ${r.statusText}`);
try {
Logger(await (await r.blob()).text(), LOG_LEVEL_VERBOSE);
} catch (_) {
Logger("Cloud not parse response", LOG_LEVEL_VERBOSE);
}
} else {
Logger(`Just checkpoint or some server information has been missing. The 404 error shown above is not an error.`, LOG_LEVEL_VERBOSE)
}
}
return response;
@ -423,7 +431,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
if (target) {
const targetItem = notes.find(e => e.dispPath == target)!;
this.resolveConflicted(targetItem.path);
await this.conflictCheckQueue.waitForPipeline();
await this.conflictCheckQueue.waitForAllProcessed();
return true;
}
return false;
@ -845,12 +853,56 @@ Note: We can always able to read V1 format. It will be progressively converted.
VIEW_TYPE_LOG,
(leaf) => new LogPaneView(leaf, this)
);
// eslint-disable-next-line no-unused-labels
TEST: {
this.registerView(
VIEW_TYPE_TEST,
(leaf) => new TestPaneView(leaf, this)
);
(async () => {
if (await this.vaultAccess.adapterExists("_SHOWDIALOGAUTO.md"))
this.showView(VIEW_TYPE_TEST);
})()
this.addCommand({
id: "view-test",
name: "Open Test dialogue",
callback: () => {
this.showView(VIEW_TYPE_TEST);
}
});
}
}
async onload() {
logStore.pipeTo(new QueueProcessor(logs => logs.forEach(e => this.addLog(e.message, e.level, e.key)), { suspended: false, batchSize: 20, concurrentLimit: 1, delay: 0 })).startPipeline();
Logger("loading plugin");
this.addSettingTab(new ObsidianLiveSyncSettingTab(this.app, this));
// eslint-disable-next-line no-unused-labels
DEV: {
__onMissingTranslation((key) => {
const now = new Date();
const filename = `missing-translation-`
const time = now.toISOString().split("T")[0];
const outFile = `${filename}${time}.jsonl`;
const piece = JSON.stringify(
{
[key]: {}
}
)
const writePiece = piece.substring(1, piece.length - 1) + ",";
fireAndForget(async () => {
try {
await this.vaultAccess.adapterAppend(this.app.vault.configDir + "/ls-debug/" + outFile, writePiece + "\n")
} catch (ex) {
Logger(`Could not write ${outFile}`, LOG_LEVEL_VERBOSE);
Logger(`Missing translation: ${writePiece}`, LOG_LEVEL_VERBOSE);
}
});
})
}
this.settingTab = new ObsidianLiveSyncSettingTab(this.app, this);
this.addSettingTab(this.settingTab);
this.addUIs();
//@ts-ignore
const manifestVersion: string = MANIFEST_VERSION || "0.0.0";
@ -860,7 +912,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
this.manifestVersion = manifestVersion;
this.packageVersion = packageVersion;
Logger(`Self-hosted LiveSync v${manifestVersion} ${packageVersion} `);
Logger($f`Self-hosted LiveSync${" v"}${manifestVersion} ${packageVersion}`);
await this.loadSettings();
const lsKey = "obsidian-live-sync-ver" + this.getVaultName();
const last_version = localStorage.getItem(lsKey);
@ -871,7 +923,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
}
const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000);
if (lastVersion > this.settings.lastReadUpdates && this.settings.isConfigured) {
Logger("Self-hosted LiveSync has undergone a major upgrade. Please open the setting dialog, and check the information pane.", LOG_LEVEL_NOTICE);
Logger($f`Self-hosted LiveSync has undergone a major upgrade. Please open the setting dialog, and check the information pane.`, LOG_LEVEL_NOTICE);
}
//@ts-ignore
@ -886,7 +938,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
this.settings.syncOnFileOpen = false;
this.settings.syncAfterMerge = false;
this.settings.periodicReplication = false;
this.settings.versionUpFlash = "Self-hosted LiveSync has been upgraded and some behaviors have changed incompatibly. All automatic synchronization is now disabled temporary. Ensure that other devices are also upgraded, and enable synchronization again.";
this.settings.versionUpFlash = $f`Self-hosted LiveSync has been upgraded and some behaviors have changed incompatibly. All automatic synchronization is now disabled temporary. Ensure that other devices are also upgraded, and enable synchronization again.`;
this.saveSettings();
}
localStorage.setItem(lsKey, `${VER}`);
@ -931,6 +983,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
onunload() {
cancelAllPeriodicTask();
cancelAllTasks();
stopAllRunningProcessors();
this._unloaded = true;
for (const addOn of this.addOns) {
addOn.onunload();
@ -943,7 +996,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
this.replicator.closeReplication();
this.localDatabase.close();
}
Logger("unloading plugin");
Logger($f`unloading plugin`);
}
async openDatabase() {
@ -951,7 +1004,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
await this.localDatabase.close();
}
const vaultName = this.getVaultName();
Logger("Waiting for ready...");
Logger($f`Waiting for ready...`);
this.localDatabase = new LiveSyncLocalDB(vaultName, this);
initializeStores(vaultName);
return await this.localDatabase.initializeDatabase();
@ -1053,6 +1106,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
}
this.settings = settings;
setLang(this.settings.displayLanguage);
if ("workingEncrypt" in this.settings) delete this.settings.workingEncrypt;
if ("workingPassphrase" in this.settings) delete this.settings.workingPassphrase;
@ -1080,6 +1134,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
this.deviceAndVaultName = localStorage.getItem(lsKey) || "";
this.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim());
this.fileEventQueue.delay = (!this.settings.liveSync && this.settings.batchSave) ? 5000 : 100;
this.settingTab.requestReload()
}
async saveSettingData() {
@ -1123,6 +1178,8 @@ Note: We can always able to read V1 format. It will be progressively converted.
}
await this.saveData(settings);
this.localDatabase.settings = this.settings;
setLang(this.settings.displayLanguage);
this.settingTab.requestReload();
this.fileEventQueue.delay = (!this.settings.liveSync && this.settings.batchSave) ? 5000 : 100;
this.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim());
if (this.settings.settingSyncFile != "") {
@ -1304,6 +1361,7 @@ We can perform a command in this file.
if (this._unloaded) {
Logger("Unload and remove the handler.", LOG_LEVEL_VERBOSE);
saveCommandDefinition.callback = this._initialCallback;
this._initialCallback = undefined;
} else {
Logger("Sync on Editor Save.", LOG_LEVEL_VERBOSE);
if (this.settings.syncOnEditorSave) {
@ -1424,6 +1482,8 @@ We can perform a command in this file.
const file = queue.args.file;
const lockKey = `handleFile:${file.path}`;
return await serialized(lockKey, async () => {
// TODO CHECK
// console.warn(lockKey);
const key = `file-last-proc-${queue.type}-${file.path}`;
const last = Number(await this.kvDB.get(key) || 0);
let mtime = file.mtime;
@ -1761,7 +1821,7 @@ We can perform a command in this file.
Logger(JSON.stringify(errors), LOG_LEVEL_VERBOSE);
}
this.replicationResultProcessor.enqueueAll(docs);
await this.replicationResultProcessor.waitForPipeline();
await this.replicationResultProcessor.waitForAllProcessed();
}
}
@ -2044,7 +2104,63 @@ We can perform a command in this file.
scheduleTask("log-hide", 3000, () => { this.statusLog.value = "" });
}
async askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> {
if (!this.replicator.tweakSettingsMismatched) {
return "OK";
}
const remoteSettings = this.replicator.mismatchedTweakValues;
const mustSettings = remoteSettings.map(e => extractObject(TweakValuesShouldMatchedTemplate, e));
const items = Object.entries(TweakValuesShouldMatchedTemplate);
// Making tables:
let table = `| Value name | Ours | ${mustSettings.map((_, i) => `Remote ${i + 1} |`).join("")}\n` +
`|: --- |: --- :${`|: --- :`.repeat(mustSettings.length)}|\n`
for (const v of items) {
const key = v[0] as keyof typeof TweakValuesShouldMatchedTemplate;
const value = mustSettings.map(e => e[key]);
table += `| ${confName(key)} | ${escapeMarkdownValue(this.settings[key])} | ${value.map((v) => `${escapeMarkdownValue(v)} |`).join("")}\n`;
}
const message = `
Configuration mismatching between the clients has been detected.
This can be harmful or extra capacity consumption. We have to make these value unified.
Configured values:
${table}
Please select a unification method.
However, even if we answer that you will \`Use mine\`, we will be prompted to accept it again on the other device and have to decide accept or not.`;
//TODO: apply this settings.
const CHOICE_USE_REMOTE = "Use Remote ";
const CHOICE_USR_MINE = "Use ours";
const CHOICE_DISMISS = "Dismiss";
// const ourConfig = extractObject(TweakValuesShouldMatchedTemplate, this.settings);
const CHOICE_AND_VALUES = [
...mustSettings.map((e, i) => [`${CHOICE_USE_REMOTE} ${i + 1}`, e]),
[CHOICE_USR_MINE, true],
[CHOICE_DISMISS, false]
]
const CHOICES = Object.fromEntries(CHOICE_AND_VALUES) as Record<string, TweakValues | boolean>;
const retKey = await confirmWithMessage(this, "Locked", message, Object.keys(CHOICES), CHOICE_DISMISS, 60);
if (!retKey) return "IGNORE";
const conf = CHOICES[retKey];
if (conf === true) {
await this.replicator.resetRemoteTweakSettings(this.settings);
Logger(`Tweak values on the remote server have been cleared, and will be overwritten in next synchronisation.`, LOG_LEVEL_NOTICE);
return "OK";
}
if (conf) {
this.settings = { ...this.settings, ...conf };
await this.saveSettingData();
Logger(`Tweak Values have been overwritten by the chosen one.`, LOG_LEVEL_NOTICE);
return "CHECKAGAIN";
}
return "IGNORE";
}
async replicate(showMessage: boolean = false) {
if (!this.isReady) return;
if (isLockAcquired("cleanup")) {
@ -2061,61 +2177,10 @@ We can perform a command in this file.
const ret = await this.replicator.openReplication(this.settings, false, showMessage, false);
if (!ret) {
if (this.replicator.tweakSettingsMismatched) {
const remoteSettings = this.replicator.mismatchedTweakValues;
const mustSettings = remoteSettings.map(e => extractObject(TweakValuesShouldMatchedTemplate, e));
const items = Object.entries(TweakValuesShouldMatchedTemplate);
// Making tables:
let table = `| Value name | Ours | ${mustSettings.map((_, i) => `Remote ${i + 1} |`).join("")}\n` +
`|: --- |: --- :${`|: --- :`.repeat(mustSettings.length)}|\n`
for (const v of items) {
const key = v[0] as keyof typeof TweakValuesShouldMatchedTemplate;
const value = mustSettings.map(e => e[key]);
table += `| ${confName(key)} | ${escapeMarkdownValue(this.settings[key])} | ${value.map((v) => `${escapeMarkdownValue(v)} |`).join("")}\n`;
}
const message = `
Configuration mismatching between the clients has been detected.
This can be harmful or extra capacity consumption. We have to make these value unified.
Configured values:
${table}
Please select a unification method.
However, even if we answer that you will \`Use mine\`, we will be prompted to accept it again on the other device and have to decide accept or not.`;
//TODO: apply this settings.
const CHOICE_USE_REMOTE = "Use Remote ";
const CHOICE_USR_MINE = "Use ours";
const CHOICE_DISMISS = "Dismiss";
// const ourConfig = extractObject(TweakValuesShouldMatchedTemplate, this.settings);
const CHOICE_AND_VALUES = [
...mustSettings.map((e, i) => [`${CHOICE_USE_REMOTE} ${i + 1}`, e]),
[CHOICE_USR_MINE, true],
[CHOICE_DISMISS, false]
]
const CHOICES = Object.fromEntries(CHOICE_AND_VALUES) as Record<string, TweakValues | boolean>;
const retKey = await confirmWithMessage(this, "Locked", message, Object.keys(CHOICES), CHOICE_DISMISS, 60);
if (!retKey) return;
const conf = CHOICES[retKey];
if (!conf) {
return;
}
if (conf === true) {
await this.replicator.resetRemoteTweakSettings(this.settings);
Logger(`Tweak values on the remote server have been cleared, and will be overwritten in next synchronisation.`, LOG_LEVEL_NOTICE);
return;
}
if (conf) {
this.settings = { ...this.settings, ...conf };
await this.saveSettingData();
Logger(`Tweak Values have been overwritten by the chosen one.`, LOG_LEVEL_NOTICE);
return;
}
await this.askResolvingMismatchedTweaks();
} else {
if (this.replicator.remoteLockedAndDeviceNotAccepted) {
if (this.replicator?.remoteLockedAndDeviceNotAccepted) {
if (this.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) {
Logger(`The remote database has been cleaned.`, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
await skipIfDuplicated("cleanup", async () => {
@ -2194,14 +2259,30 @@ Or if you are sure know what had been happened, we can unlock the database from
}
}
async replicateAllToServer(showingNotice: boolean = false) {
async replicateAllToServer(showingNotice: boolean = false): Promise<boolean> {
if (!this.isReady) return false;
await Promise.all(this.addOns.map(e => e.beforeReplicate(showingNotice)));
return await this.replicator.replicateAllToServer(this.settings, showingNotice);
const ret = await this.replicator.replicateAllToServer(this.settings, showingNotice);
if (ret) return true;
if (this.replicator.tweakSettingsMismatched) {
const ret = await this.askResolvingMismatchedTweaks();
if (ret == "OK") return true;
if (ret == "CHECKAGAIN") return await this.replicateAllToServer(showingNotice);
if (ret == "IGNORE") return false;
}
return ret;
}
async replicateAllFromServer(showingNotice: boolean = false) {
async replicateAllFromServer(showingNotice: boolean = false): Promise<boolean> {
if (!this.isReady) return false;
return await this.replicator.replicateAllFromServer(this.settings, showingNotice);
const ret = await this.replicator.replicateAllFromServer(this.settings, showingNotice);
if (ret) return true;
if (this.replicator.tweakSettingsMismatched) {
const ret = await this.askResolvingMismatchedTweaks();
if (ret == "OK") return true;
if (ret == "CHECKAGAIN") return await this.replicateAllFromServer(showingNotice);
if (ret == "IGNORE") return false;
}
return ret;
}
async markRemoteLocked(lockByClean: boolean = false) {
@ -2308,7 +2389,7 @@ Or if you are sure know what had been happened, we can unlock the database from
}
return;
}, { batchSize: 1, concurrentLimit: 10, delay: 0, suspended: true }, objects)
await processor.waitForPipeline();
await processor.waitForAllDoneAndTerminate();
const msg = `${procedureName} All done: DONE:${success}, FAILED:${failed}`;
updateLog(procedureName, msg)
}
@ -2376,7 +2457,7 @@ Or if you are sure know what had been happened, we can unlock the database from
}
}
processPrepareSyncFile.startPipeline().onUpdateProgress(() => remainLog(processPrepareSyncFile.totalRemaining + processPrepareSyncFile.nowProcessing))
initProcess.push(processPrepareSyncFile.waitForPipeline());
initProcess.push(processPrepareSyncFile.waitForAllDoneAndTerminate());
await Promise.all(initProcess);
// this.setStatusBarText(`NOW TRACKING!`);

50
src/tests/TestPane.svelte Normal file
View File

@ -0,0 +1,50 @@
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import type ObsidianLiveSyncPlugin from "../main";
import { perf_trench } from "./tests";
import { MarkdownRenderer } from "../deps";
export let plugin: ObsidianLiveSyncPlugin;
let performanceTestResult = "";
let functionCheckResult = "";
let testRunning = false;
let prefTestResultEl: HTMLDivElement;
let isReady = false;
$: {
if (performanceTestResult != "" && isReady) {
MarkdownRenderer.render(plugin.app, performanceTestResult, prefTestResultEl, "/", plugin);
}
}
async function performTest() {
try {
testRunning = true;
performanceTestResult = await perf_trench(plugin);
} finally {
testRunning = false;
}
}
function clearPerfTestResult() {
prefTestResultEl.empty();
}
onMount(() => {
isReady = true;
// performTest();
});
</script>
<h2>TESTBENCH: Self-hosted LiveSync</h2>
<h3>Function check</h3>
<pre>{functionCheckResult}</pre>
<h3>Performance test</h3>
<button on:click={() => performTest()} disabled={testRunning}>Test!</button>
<button on:click={() => clearPerfTestResult()}>Clear</button>
<div bind:this={prefTestResultEl}></div>
<style>
* {
box-sizing: border-box;
}
</style>

49
src/tests/TestPaneView.ts Normal file
View File

@ -0,0 +1,49 @@
import {
ItemView,
WorkspaceLeaf
} from "obsidian";
import TestPaneComponent from "./TestPane.svelte"
import type ObsidianLiveSyncPlugin from "../main"
export const VIEW_TYPE_TEST = "ols-pane-test";
//Log view
export class TestPaneView extends ItemView {
component?: TestPaneComponent;
plugin: ObsidianLiveSyncPlugin;
icon = "view-log";
title: string = "Self-hosted LiveSync Test and Results"
navigation = true;
getIcon(): string {
return "view-log";
}
constructor(leaf: WorkspaceLeaf, plugin: ObsidianLiveSyncPlugin) {
super(leaf);
this.plugin = plugin;
}
getViewType() {
return VIEW_TYPE_TEST;
}
getDisplayText() {
return "Self-hosted LiveSync Test and Results";
}
// eslint-disable-next-line require-await
async onOpen() {
this.component = new TestPaneComponent({
target: this.contentEl,
props: {
plugin: this.plugin
},
});
}
// eslint-disable-next-line require-await
async onClose() {
this.component?.$destroy();
}
}

70
src/tests/tests.ts Normal file
View File

@ -0,0 +1,70 @@
import { Trench } from "../lib/src/memory/memutil.ts";
import type ObsidianLiveSyncPlugin from "../main.ts";
type MeasureResult = [times: number, spent: number];
type NamedMeasureResult = [name: string, result: MeasureResult];
const measures = new Map<string, MeasureResult>();
function clearResult(name: string) {
measures.set(name, [0, 0]);
}
async function measureEach(name: string, proc: () => (void | Promise<void>)) {
const [times, spent] = measures.get(name) ?? [0, 0];
const start = performance.now();
const result = proc();
if (result instanceof Promise) await result;
const end = performance.now();
measures.set(name, [times + 1, spent + (end - start)]);
}
function formatNumber(num: number) {
return num.toLocaleString('en-US', { maximumFractionDigits: 2 });
}
async function measure(name: string, proc: () => (void | Promise<void>), times: number = 10000, duration: number = 1000): Promise<NamedMeasureResult> {
const from = Date.now();
let last = times;
clearResult(name);
do {
await measureEach(name, proc);
} while (last-- > 0 && (Date.now() - from) < duration)
return [name, measures.get(name) as MeasureResult];
}
// eslint-disable-next-line require-await
async function formatPerfResults(items: NamedMeasureResult[]) {
return `| Name | Runs | Each | Total |\n| --- | --- | --- | --- | \n` + items.map(e => `| ${e[0]} | ${e[1][0]} | ${e[1][0] != 0 ? formatNumber(e[1][1] / e[1][0]) : "-"} | ${formatNumber(e[1][0])} |`).join("\n");
}
export async function perf_trench(plugin: ObsidianLiveSyncPlugin) {
clearResult("trench");
const trench = new Trench(plugin.simpleStore);
const result = [] as NamedMeasureResult[];
result.push(await measure("trench-short-string", async () => {
const p = trench.evacuate("string");
await p();
}));
{
const testBinary = await plugin.vaultAccess.adapterReadBinary("testdata/10kb.png");
const uint8Array = new Uint8Array(testBinary);
result.push(await measure("trench-binary-10kb", async () => {
const p = trench.evacuate(uint8Array);
await p();
}));
}
{
const testBinary = await plugin.vaultAccess.adapterReadBinary("testdata/100kb.jpeg");
const uint8Array = new Uint8Array(testBinary);
result.push(await measure("trench-binary-100kb", async () => {
const p = trench.evacuate(uint8Array);
await p();
}));
}
{
const testBinary = await plugin.vaultAccess.adapterReadBinary("testdata/1mb.png");
const uint8Array = new Uint8Array(testBinary);
result.push(await measure("trench-binary-1mb", async () => {
const p = trench.evacuate(uint8Array);
await p();
}));
}
return formatPerfResults(result);
}

View File

@ -226,12 +226,18 @@
<td class="path">
<div class="filenames">
<span class="path">/{entry.dirname.split("/").join(`​/`)}</span>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-missing-attribute -->
<span class="filename"><a on:click={() => openFile(entry.path)}>{entry.filename}</a></span>
</div>
</td>
<td>
<span class="rev">
{#if entry.isPlain}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-missing-attribute -->
<a on:click={() => showHistory(entry.path, entry?.rev || "")}>{entry.rev}</a>
{:else}
{entry.rev}

View File

@ -8,11 +8,11 @@ import type ObsidianLiveSyncPlugin from "../main.ts";
export const VIEW_TYPE_GLOBAL_HISTORY = "global-history";
export class GlobalHistoryView extends ItemView {
component: GlobalHistoryComponent;
component?: GlobalHistoryComponent;
plugin: ObsidianLiveSyncPlugin;
icon: "clock";
title: string;
navigation: true;
icon = "clock";
title: string = "";
navigation = true;
getIcon(): string {
return "clock";
@ -44,6 +44,6 @@ export class GlobalHistoryView extends ItemView {
// eslint-disable-next-line require-await
async onClose() {
this.component.$destroy();
this.component?.$destroy();
}
}

View File

@ -6,27 +6,27 @@ import { waitForSignal } from "../lib/src/common/utils.ts";
export class JsonResolveModal extends Modal {
// result: Array<[number, string]>;
filename: FilePath;
callback: (keepRev: string, mergedStr?: string) => Promise<void>;
callback?: (keepRev?: string, mergedStr?: string) => Promise<void>;
docs: LoadedEntry[];
component: JsonResolvePane;
component?: JsonResolvePane;
nameA: string;
nameB: string;
defaultSelect: string;
constructor(app: App, filename: FilePath, docs: LoadedEntry[], callback: (keepRev: string, mergedStr?: string) => Promise<void>, nameA?: string, nameB?: string, defaultSelect?: string) {
constructor(app: App, filename: FilePath, docs: LoadedEntry[], callback: (keepRev?: string, mergedStr?: string) => Promise<void>, nameA?: string, nameB?: string, defaultSelect?: string) {
super(app);
this.callback = callback;
this.filename = filename;
this.docs = docs;
this.nameA = nameA;
this.nameB = nameB;
this.defaultSelect = defaultSelect;
this.nameA = nameA || "";
this.nameB = nameB || "";
this.defaultSelect = defaultSelect || "";
waitForSignal(`cancel-internal-conflict:${filename}`).then(() => this.close());
}
async UICallback(keepRev: string, mergedStr?: string) {
async UICallback(keepRev?: string, mergedStr?: string) {
this.close();
await this.callback(keepRev, mergedStr);
this.callback = null;
await this.callback?.(keepRev, mergedStr);
this.callback = undefined;
}
onOpen() {
@ -34,7 +34,7 @@ export class JsonResolveModal extends Modal {
this.titleEl.setText("Conflicted Setting");
contentEl.empty();
if (this.component == null) {
if (this.component == undefined) {
this.component = new JsonResolvePane({
target: contentEl,
props: {
@ -43,7 +43,7 @@ export class JsonResolveModal extends Modal {
nameA: this.nameA,
nameB: this.nameB,
defaultSelect: this.defaultSelect,
callback: (keepRev: string, mergedStr: string) => this.UICallback(keepRev, mergedStr),
callback: (keepRev, mergedStr) => this.UICallback(keepRev, mergedStr),
},
});
}
@ -55,12 +55,12 @@ export class JsonResolveModal extends Modal {
const { contentEl } = this;
contentEl.empty();
// contentEl.empty();
if (this.callback != null) {
this.callback(null);
if (this.callback != undefined) {
this.callback(undefined);
}
if (this.component != null) {
if (this.component != undefined) {
this.component.$destroy();
this.component = null;
this.component = undefined;
}
}
}

View File

@ -1,41 +0,0 @@
import { App, Modal } from "../deps.ts";
import type { ReactiveInstance, } from "../lib/src/dataobject/reactive.ts";
import { logMessages } from "../lib/src/mock_and_interop/stores.ts";
import { escapeStringToHTML } from "../lib/src/string_and_binary/strbin.ts";
import ObsidianLiveSyncPlugin from "../main.ts";
export class LogDisplayModal extends Modal {
plugin: ObsidianLiveSyncPlugin;
logEl: HTMLDivElement;
unsubscribe: () => void;
constructor(app: App, plugin: ObsidianLiveSyncPlugin) {
super(app);
this.plugin = plugin;
}
onOpen() {
const { contentEl } = this;
this.titleEl.setText("Sync status");
contentEl.empty();
const div = contentEl.createDiv("");
div.addClass("op-scrollable");
div.addClass("op-pre");
this.logEl = div;
function updateLog(logs: ReactiveInstance<string[]>) {
const e = logs.value;
let msg = "";
for (const v of e) {
msg += escapeStringToHTML(v) + "<br>";
}
this.logEl.innerHTML = msg;
}
logMessages.onChanged(updateLog);
this.unsubscribe = () => logMessages.offChanged(updateLog);
}
onClose() {
const { contentEl } = this;
contentEl.empty();
if (this.unsubscribe) this.unsubscribe();
}
}

View File

@ -8,11 +8,11 @@ export const VIEW_TYPE_LOG = "log-log";
//Log view
export class LogPaneView extends ItemView {
component: LogPaneComponent;
component?: LogPaneComponent;
plugin: ObsidianLiveSyncPlugin;
icon: "view-log";
title: string;
navigation: true;
icon = "view-log";
title: string = "";
navigation = true;
getIcon(): string {
return "view-log";
@ -43,6 +43,6 @@ export class LogPaneView extends ItemView {
// eslint-disable-next-line require-await
async onClose() {
this.component.$destroy();
this.component?.$destroy();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -366,10 +366,10 @@
</div>
<style>
span.spacer {
/* span.spacer {
min-width: 1px;
flex-grow: 1;
}
} */
h3 {
position: sticky;
top: 0;

View File

@ -31,6 +31,7 @@
<ul>
{#each patterns as pattern, idx}
<!-- svelte-ignore a11y-label-has-associated-control -->
<li><label>{modified[idx]}{status[idx]}</label><input type="text" bind:value={pattern} class={modified[idx]} /><button class="iconbutton" on:click={() => remove(idx)}>🗑</button></li>
{/each}
<li>
@ -72,12 +73,7 @@
li input {
min-width: 10em;
}
li.buttons {
}
button.iconbutton {
max-width: 4em;
}
span.spacer {
flex-grow: 1;
}
</style>

329
src/ui/settingConstants.ts Normal file
View File

@ -0,0 +1,329 @@
import { $t } from "src/lib/src/common/i18n";
import { DEFAULT_SETTINGS, configurationNames, type ConfigurationItem, type FilterBooleanKeys, type FilterNumberKeys, type FilterStringKeys, type ObsidianLiveSyncSettings } from "src/lib/src/common/types";
export type OnDialogSettings = {
configPassphrase: string,
preset: "" | "PERIODIC" | "LIVESYNC" | "DISABLE",
syncMode: "ONEVENTS" | "PERIODIC" | "LIVESYNC"
dummy: number,
}
export const OnDialogSettingsDefault: OnDialogSettings = {
configPassphrase: "",
preset: "",
syncMode: "ONEVENTS",
dummy: 0,
}
export const AllSettingDefault =
{ ...DEFAULT_SETTINGS, ...OnDialogSettingsDefault }
export type AllSettings = ObsidianLiveSyncSettings & OnDialogSettings;
export type AllStringItemKey = FilterStringKeys<AllSettings>;
export type AllNumericItemKey = FilterNumberKeys<AllSettings>;
export type AllBooleanItemKey = FilterBooleanKeys<AllSettings>;
export type AllSettingItemKey = AllStringItemKey | AllNumericItemKey | AllBooleanItemKey;
export type ValueOf<T extends AllSettingItemKey> =
T extends AllStringItemKey ? string :
T extends AllNumericItemKey ? number :
T extends AllBooleanItemKey ? boolean :
AllSettings[T];
export const SettingInformation: Partial<Record<keyof AllSettings, ConfigurationItem>> = {
"liveSync": {
"name": "Sync Mode"
},
"couchDB_URI": {
"name": "URI",
"placeHolder": "https://........"
},
"couchDB_USER": {
"name": "Username",
"desc": "username"
},
"couchDB_PASSWORD": {
"name": "Password",
"desc": "password"
},
"couchDB_DBNAME": {
"name": "Database name"
},
"passphrase": {
"name": "Passphrase",
"desc": "Encrypting passphrase. If you change the passphrase of an existing database, overwriting the remote database is strongly recommended."
},
"showStatusOnEditor": {
"name": "Show status inside the editor",
"desc": "Reflected after reboot"
},
"showOnlyIconsOnEditor": {
"name": "Show status as icons only"
},
"showStatusOnStatusbar": {
"name": "Show status on the status bar",
"desc": "Reflected after reboot."
},
"lessInformationInLog": {
"name": "Show only notifications",
"desc": "Prevent logging and show only notification"
},
"showVerboseLog": {
"name": "Verbose Log",
"desc": "Show verbose log"
},
"hashCacheMaxCount": {
"name": "Memory cache size (by total items)"
},
"hashCacheMaxAmount": {
"name": "Memory cache size (by total characters)",
"desc": "(Mega chars)"
},
"writeCredentialsForSettingSync": {
"name": "Write credentials in the file",
"desc": "(Not recommended) If set, credentials will be stored in the file."
},
"notifyAllSettingSyncFile": {
"name": "Notify all setting files"
},
"configPassphrase": {
"name": "Passphrase of sensitive configuration items",
"desc": "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again."
},
"configPassphraseStore": {
"name": "Encrypting sensitive configuration items"
},
"syncOnSave": {
"name": "Sync on Save",
"desc": "When you save a file, sync automatically"
},
"syncOnEditorSave": {
"name": "Sync on Editor Save",
"desc": "When you save a file in the editor, sync automatically"
},
"syncOnFileOpen": {
"name": "Sync on File Open",
"desc": "When you open a file, sync automatically"
},
"syncOnStart": {
"name": "Sync on Start",
"desc": "Start synchronization after launching Obsidian."
},
"syncAfterMerge": {
"name": "Sync after merging file",
"desc": "Sync automatically after merging files"
},
"trashInsteadDelete": {
"name": "Use the trash bin",
"desc": "Do not delete files that are deleted in remote, just move to trash."
},
"doNotDeleteFolder": {
"name": "Keep empty folder",
"desc": "Normally, a folder is deleted when it becomes empty after a synchronization. Enabling this will prevent it from getting deleted"
},
"resolveConflictsByNewerFile": {
"name": "Always overwrite with a newer file (beta)",
"desc": "(Def off) Resolve conflicts by newer files automatically."
},
"checkConflictOnlyOnOpen": {
"name": "Postpone resolution of inactive files"
},
"showMergeDialogOnlyOnActive": {
"name": "Postpone manual resolution of inactive files"
},
"disableMarkdownAutoMerge": {
"name": "Always resolve conflicts manually",
"desc": "If this switch is turned on, a merge dialog will be displayed, even if the sensible-merge is possible automatically. (Turn on to previous behavior)"
},
"writeDocumentsIfConflicted": {
"name": "Always reflect synchronized changes even if the note has a conflict",
"desc": "Turn on to previous behavior"
},
"syncInternalFilesInterval": {
"name": "Scan hidden files periodically",
"desc": "Seconds, 0 to disable"
},
"batchSave": {
"name": "Batch database update",
"desc": "Reducing the frequency with which on-disk changes are reflected into the DB"
},
"readChunksOnline": {
"name": "Fetch chunks on demand",
"desc": "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended."
},
"syncMaxSizeInMB": {
"name": "Maximum file size",
"desc": "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used."
},
"useIgnoreFiles": {
"name": "(Beta) Use ignore files",
"desc": "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files."
},
"ignoreFiles": {
"name": "Ignore files",
"desc": "We can use multiple ignore files, e.g.) `.gitignore, .dockerignore`"
},
"batch_size": {
"name": "Batch size",
"desc": "Number of change feed items to process at a time. Defaults to 50. Minimum is 2."
},
"batches_limit": {
"name": "Batch limit",
"desc": "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time."
},
"useTimeouts": {
"name": "Use timeouts instead of heartbeats",
"desc": "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage."
},
"concurrencyOfReadChunksOnline": {
"name": "Batch size of on-demand fetching"
},
"minimumIntervalOfReadChunksOnline": {
"name": "The delay for consecutive on-demand fetches"
},
"suspendFileWatching": {
"name": "Suspend file watching",
"desc": "Stop watching for file change."
},
"suspendParseReplicationResult": {
"name": "Suspend database reflecting",
"desc": "Stop reflecting database changes to storage files."
},
"writeLogToTheFile": {
"name": "Write logs into the file",
"desc": "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information."
},
"deleteMetadataOfDeletedFiles": {
"name": "Do not keep metadata of deleted files."
},
"useIndexedDBAdapter": {
"name": "Use an old adapter for compatibility",
"desc": "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this."
},
"watchInternalFileChanges": {
"name": "Scan changes on customization sync",
"desc": "Do not use internal API"
},
"doNotSuspendOnFetching": {
"name": "Fetch database with previous behaviour"
},
"disableCheckingConfigMismatch": {
"name": "Do not check configuration mismatch before replication"
},
"usePluginSync": {
"name": "Enable customization sync"
},
"autoSweepPlugins": {
"name": "Scan customization automatically",
"desc": "Scan customization before replicating."
},
"autoSweepPluginsPeriodic": {
"name": "Scan customization periodically",
"desc": "Scan customization every 1 minute."
},
"notifyPluginOrSettingUpdated": {
"name": "Notify customized",
"desc": "Notify when other device has newly customized."
},
"remoteType": {
"name": "Remote Type",
"desc": "Remote server type"
},
"endpoint": {
"name": "Endpoint URL",
"placeHolder": "https://........"
},
"accessKey": {
"name": "Access Key"
},
"secretKey": {
"name": "Secret Key"
},
"region": {
"name": "Region",
"placeHolder": "auto"
},
"bucket": {
"name": "Bucket Name"
},
"useCustomRequestHandler": {
"name": "Use Custom HTTP Handler",
"desc": "If your Object Storage could not configured accepting CORS, enable this."
},
"maxChunksInEden": {
"name": "Maximum Incubating Chunks",
"desc": "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks."
},
"maxTotalLengthInEden": {
"name": "Maximum Incubating Chunk Size",
"desc": "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks."
},
"maxAgeInEden": {
"name": "Maximum Incubation Period",
"desc": "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks."
},
"settingSyncFile": {
"name": "Filename",
"desc": "If you set this, all settings are saved in a markdown file. You will be notified when new settings arrive. You can set different files by the platform."
},
"preset": {
"name": "Presets",
"desc": "Apply preset configuration"
},
"syncMode": {
name: "Sync Mode",
},
"periodicReplicationInterval": {
"name": "Periodic Sync interval",
"desc": "Interval (sec)"
},
"syncInternalFilesBeforeReplication": {
"name": "Scan for hidden files before replication"
},
"automaticallyDeleteMetadataOfDeletedFiles": {
"name": "Delete old metadata of deleted files on start-up",
"desc": "(Days passed, 0 to disable automatic-deletion)"
},
"additionalSuffixOfDatabaseName": {
"name": "Database suffix",
"desc": "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured."
},
"hashAlg": {
"name": configurationNames["hashAlg"]?.name || "",
"desc": "xxhash64 is the current default."
},
"deviceAndVaultName": {
"name": "Device name",
"desc": "Unique name between all synchronized devices. To edit this setting, please disable customization sync once."
},
"displayLanguage": {
"name": "Display Language",
"desc": "Not all messages have been translated. And, please revert to \"Default\" when reporting errors."
}
}
function translateInfo(infoSrc: ConfigurationItem | undefined | false) {
if (!infoSrc) return false;
const info = { ...infoSrc };
info.name = $t(info.name);
if (info.desc) {
info.desc = $t(info.desc);
}
return info;
}
function _getConfig(key: AllSettingItemKey) {
if (key in configurationNames) {
return configurationNames[key as keyof ObsidianLiveSyncSettings];
}
if (key in SettingInformation) {
return SettingInformation[key as keyof ObsidianLiveSyncSettings];
}
return false;
}
export function getConfig(key: AllSettingItemKey) {
return translateInfo(_getConfig(key));
}
export function getConfName(key: AllSettingItemKey) {
const conf = getConfig(key);
if (!conf) return `${key} (No info)`;
return conf.name;
}

View File

@ -133,6 +133,7 @@
top: var(--view-header-height);
right: 1em;
}
.canvas-wrapper::before {
right: 48px;
}
@ -270,6 +271,22 @@ div.sls-setting-menu-btn {
content: "✏";
}
.sls-item-dirty-help::after {
content: " ❓";
}
.sls-item-invalid-value {
background-color: rgba(var(--background-modifier-error-rgb), 0.3) !important;
}
.sls-setting-disabled input[type=text],
.sls-setting-disabled input[type=number],
.sls-setting-disabled input[type=password] {
filter: brightness(80%);
color: var(--text-muted);
}
.sls-setting-hidden {
display: none;
}