mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2025-12-15 10:45:59 +00:00
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:
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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("");
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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();
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
2
src/lib
2
src/lib
Submodule src/lib updated: 70eb916288...6548bd3ed7
243
src/main.ts
243
src/main.ts
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
src/utils.ts
10
src/utils.ts
@@ -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 };
|
||||||
|
|||||||
Reference in New Issue
Block a user