mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2025-02-07 19:30:08 +02: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:
parent
5acbbe479e
commit
bcce277c36
@ -1,14 +1,14 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { Notice, type PluginManifest, parseYaml } from "./deps";
|
||||
import { Notice, type PluginManifest, parseYaml, normalizePath } from "./deps";
|
||||
|
||||
import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, DocumentID, AnyEntry } from "./lib/src/types";
|
||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "./lib/src/types";
|
||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE } from "./lib/src/types";
|
||||
import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "./types";
|
||||
import { delay, getDocData } from "./lib/src/utils";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { WrappedNotice } from "./lib/src/wrapper";
|
||||
import { base64ToArrayBuffer, arrayBufferToBase64, readString, crc32CKHash } from "./lib/src/strbin";
|
||||
import { runWithLock } from "./lib/src/lock";
|
||||
import { serialized } from "./lib/src/lock";
|
||||
import { LiveSyncCommands } from "./LiveSyncCommands";
|
||||
import { stripAllPrefixes } from "./lib/src/path";
|
||||
import { PeriodicProcessor, askYesNo, disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "./utils";
|
||||
@ -16,10 +16,20 @@ import { PluginDialogModal } from "./dialogs";
|
||||
import { JsonResolveModal } from "./JsonResolveModal";
|
||||
import { pipeGeneratorToGenerator, processAllGeneratorTasksWithConcurrencyLimit } from './lib/src/task';
|
||||
|
||||
|
||||
function serialize<T>(obj: T): string {
|
||||
return JSON.stringify(obj, null, 1);
|
||||
function serialize(data: PluginDataEx): string {
|
||||
// To improve performance, make JSON manually.
|
||||
// Self-hosted LiveSync uses `\n` to split chunks. Therefore, grouping together those with similar entropy would work nicely.
|
||||
return `{"category":"${data.category}","name":"${data.name}","term":${JSON.stringify(data.term)}
|
||||
${data.version ? `,"version":"${data.version}"` : ""},
|
||||
"mtime":${data.mtime},
|
||||
"files":[
|
||||
${data.files.map(file => `{"filename":"${file.filename}"${file.displayName ? `,"displayName":"${file.displayName}"` : ""}${file.version ? `,"version":"${file.version}"` : ""},
|
||||
"mtime":${file.mtime},"size":${file.size}
|
||||
,"data":[${file.data.map(e => `"${e}"`).join(",")
|
||||
}]}`).join(",")
|
||||
}]}`
|
||||
}
|
||||
|
||||
function deserialize<T>(str: string, def: T) {
|
||||
try {
|
||||
return JSON.parse(str) as T;
|
||||
@ -107,6 +117,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getFileCategory(filePath: string): "CONFIG" | "THEME" | "SNIPPET" | "PLUGIN_MAIN" | "PLUGIN_ETC" | "PLUGIN_DATA" | "" {
|
||||
if (filePath.split("/").length == 2 && filePath.endsWith(".json")) return "CONFIG";
|
||||
if (filePath.split("/").length == 4 && filePath.startsWith(`${this.app.vault.configDir}/themes/`)) return "THEME";
|
||||
@ -164,6 +175,46 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
pluginList.set(this.pluginList)
|
||||
await this.updatePluginList(showMessage);
|
||||
}
|
||||
async loadPluginData(path: FilePathWithPrefix): Promise<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> {
|
||||
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||
// pluginList.set([]);
|
||||
@ -174,7 +225,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
}
|
||||
await Promise.resolve(); // Just to prevent warning.
|
||||
scheduleTask("update-plugin-list-task", 200, async () => {
|
||||
await runWithLock("update-plugin-list", false, async () => {
|
||||
await serialized("update-plugin-list", async () => {
|
||||
try {
|
||||
const updatedDocumentId = updatedDocumentPath ? await this.path2id(updatedDocumentPath) : "";
|
||||
const plugins = updatedDocumentPath ?
|
||||
@ -193,22 +244,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
count++;
|
||||
if (count % 10 == 0) Logger(`Enumerating files... ${count}`, logLevel, "get-plugins");
|
||||
Logger(`plugin-${path}`, LOG_LEVEL_VERBOSE);
|
||||
const wx = await this.localDatabase.getDBEntry(path, null, false, false);
|
||||
if (wx) {
|
||||
const data = deserialize(getDocData(wx.data), {}) as PluginDataEx;
|
||||
const xFiles = [] as PluginDataExFile[];
|
||||
for (const file of data.files) {
|
||||
const work = { ...file };
|
||||
const tempStr = getDocData(work.data);
|
||||
work.data = [crc32CKHash(tempStr)];
|
||||
xFiles.push(work);
|
||||
}
|
||||
return ({
|
||||
...data,
|
||||
documentPath: this.getPath(wx),
|
||||
files: xFiles
|
||||
});
|
||||
}
|
||||
return this.loadPluginData(path);
|
||||
// return entries;
|
||||
} catch (ex) {
|
||||
//TODO
|
||||
@ -218,7 +254,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
return false;
|
||||
}))) {
|
||||
if ("ok" in v) {
|
||||
if (v.ok != false) {
|
||||
if (v.ok !== false) {
|
||||
let newList = [...this.pluginList];
|
||||
const item = v.ok;
|
||||
newList = newList.filter(x => x.documentPath != item.documentPath);
|
||||
@ -230,6 +266,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
}
|
||||
}
|
||||
Logger(`All files enumerated`, logLevel, "get-plugins");
|
||||
this.createMissingConfigurationEntry();
|
||||
} finally {
|
||||
pluginIsEnumerating.set(false);
|
||||
}
|
||||
@ -257,7 +294,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
const fileA = { ...pluginDataA.files[0], ctime: pluginDataA.files[0].mtime, _id: `${pluginDataA.documentPath}` as DocumentID };
|
||||
const fileB = pluginDataB.files[0];
|
||||
const docAx = { ...docA, ...fileA } as LoadedEntry, docBx = { ...docB, ...fileB } as LoadedEntry
|
||||
return runWithLock("config:merge-data", false, () => new Promise((res) => {
|
||||
return serialized("config:merge-data", () => new Promise((res) => {
|
||||
Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE);
|
||||
// const docs = [docA, docB];
|
||||
const path = stripAllPrefixes(docAx.path.split("/").slice(-1).join("/") as FilePath);
|
||||
@ -411,6 +448,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
this.periodicPluginSweepProcessor.enable(this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges ? (PERIODIC_PLUGIN_SWEEP * 1000) : 0);
|
||||
return;
|
||||
}
|
||||
|
||||
recentProcessedInternalFiles = [] as string[];
|
||||
async makeEntryFromFile(path: FilePath): Promise<false | PluginDataExFile> {
|
||||
const stat = await this.app.vault.adapter.stat(path);
|
||||
@ -470,7 +508,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
return;
|
||||
}
|
||||
const vf = this.filenameToUnifiedKey(path, term);
|
||||
return await runWithLock(`plugin-${vf}`, false, async () => {
|
||||
return await serialized(`plugin-${vf}`, async () => {
|
||||
const category = this.getFileCategory(path);
|
||||
let mtime = 0;
|
||||
let fileTargets = [] as FilePath[];
|
||||
@ -578,6 +616,13 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
// Make sure that target is a file.
|
||||
if (stat && stat.type != "file")
|
||||
return false;
|
||||
|
||||
const configDir = normalizePath(this.app.vault.configDir);
|
||||
const synchronisedInConfigSync = Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode != MODE_SELECTIVE).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase());
|
||||
if (synchronisedInConfigSync.some(e => e.startsWith(path.toLowerCase()))) {
|
||||
Logger(`Customization file skipped: ${path}`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
const storageMTime = ~~((stat && stat.mtime || 0) / 1000);
|
||||
const key = `${path}-${storageMTime}`;
|
||||
if (this.recentProcessedInternalFiles.contains(key)) {
|
||||
@ -618,7 +663,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
|
||||
// const id = await this.path2id(prefixedFileName);
|
||||
const mtime = new Date().getTime();
|
||||
await runWithLock("file-x-" + prefixedFileName, false, async () => {
|
||||
await serialized("file-x-" + prefixedFileName, async () => {
|
||||
try {
|
||||
const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, null, false) as InternalFileEntry | false;
|
||||
let saveData: InternalFileEntry;
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { Notice, normalizePath, type PluginManifest } from "./deps";
|
||||
import { type EntryDoc, type LoadedEntry, type InternalFileEntry, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "./lib/src/types";
|
||||
import { normalizePath, type PluginManifest } from "./deps";
|
||||
import { type EntryDoc, type LoadedEntry, type InternalFileEntry, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_PAUSED } from "./lib/src/types";
|
||||
import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "./types";
|
||||
import { Parallels, delay, isDocContentSame } from "./lib/src/utils";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
||||
import { disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask, isInternalMetadata, PeriodicProcessor } from "./utils";
|
||||
import { scheduleTask, isInternalMetadata, PeriodicProcessor } from "./utils";
|
||||
import { WrappedNotice } from "./lib/src/wrapper";
|
||||
import { base64ToArrayBuffer, arrayBufferToBase64 } from "./lib/src/strbin";
|
||||
import { runWithLock } from "./lib/src/lock";
|
||||
import { serialized } from "./lib/src/lock";
|
||||
import { JsonResolveModal } from "./JsonResolveModal";
|
||||
import { LiveSyncCommands } from "./LiveSyncCommands";
|
||||
import { addPrefix, stripAllPrefixes } from "./lib/src/path";
|
||||
@ -77,7 +77,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
|
||||
procInternalFiles: string[] = [];
|
||||
async execInternalFile() {
|
||||
await runWithLock("execInternal", false, async () => {
|
||||
await serialized("execInternal", async () => {
|
||||
const w = [...this.procInternalFiles];
|
||||
this.procInternalFiles = [];
|
||||
Logger(`Applying hidden ${w.length} files change...`);
|
||||
@ -95,6 +95,14 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
recentProcessedInternalFiles = [] as string[];
|
||||
async watchVaultRawEventsAsync(path: FilePath) {
|
||||
if (!this.settings.syncInternalFiles) return;
|
||||
|
||||
// Exclude files handled by customization sync
|
||||
const configDir = normalizePath(this.app.vault.configDir);
|
||||
const synchronisedInConfigSync = !this.settings.usePluginSync ? [] : Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode == MODE_SELECTIVE || e.mode == MODE_PAUSED).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase());
|
||||
if (synchronisedInConfigSync.some(e => e.startsWith(path.toLowerCase()))) {
|
||||
Logger(`Hidden file skipped: ${path} is synchronized in customization sync.`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
const stat = await this.app.vault.adapter.stat(path);
|
||||
// sometimes folder is coming.
|
||||
if (stat && stat.type != "file")
|
||||
@ -209,18 +217,24 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
}
|
||||
|
||||
//TODO: Tidy up. Even though it is experimental feature, So dirty...
|
||||
async syncInternalFilesAndDatabase(direction: "push" | "pull" | "safe" | "pullForce" | "pushForce", showMessage: boolean, files: InternalFileInfo[] | false = false, targetFiles: string[] | false = false) {
|
||||
async syncInternalFilesAndDatabase(direction: "push" | "pull" | "safe" | "pullForce" | "pushForce", showMessage: boolean, filesAll: InternalFileInfo[] | false = false, targetFiles: string[] | false = false) {
|
||||
await this.resolveConflictOnInternalFiles();
|
||||
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||
Logger("Scanning hidden files.", logLevel, "sync_internal");
|
||||
const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns
|
||||
.replace(/\n| /g, "")
|
||||
.split(",").filter(e => e).map(e => new RegExp(e, "i"));
|
||||
if (!files)
|
||||
files = await this.scanInternalFiles();
|
||||
|
||||
const configDir = normalizePath(this.app.vault.configDir);
|
||||
let files: InternalFileInfo[] =
|
||||
filesAll ? filesAll : (await this.scanInternalFiles())
|
||||
|
||||
const synchronisedInConfigSync = !this.settings.usePluginSync ? [] : Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode == MODE_SELECTIVE || e.mode == MODE_PAUSED).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase());
|
||||
files = files.filter(file => synchronisedInConfigSync.every(filterFile => !file.path.toLowerCase().startsWith(filterFile)))
|
||||
|
||||
const filesOnDB = ((await this.localDatabase.allDocsRaw({ startkey: ICHeader, endkey: ICHeaderEnd, include_docs: true })).rows.map(e => e.doc) as InternalFileEntry[]).filter(e => !e.deleted);
|
||||
const allFileNamesSrc = [...new Set([...files.map(e => normalizePath(e.path)), ...filesOnDB.map(e => stripAllPrefixes(this.getPath(e)))])];
|
||||
const allFileNames = allFileNamesSrc.filter(filename => !targetFiles || (targetFiles && targetFiles.indexOf(filename) !== -1));
|
||||
const allFileNames = allFileNamesSrc.filter(filename => !targetFiles || (targetFiles && targetFiles.indexOf(filename) !== -1)).filter(path => synchronisedInConfigSync.every(filterFile => !path.toLowerCase().startsWith(filterFile)))
|
||||
function compareMTime(a: number, b: number) {
|
||||
const wa = ~~(a / 1000);
|
||||
const wb = ~~(b / 1000);
|
||||
@ -274,7 +288,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
if (ignorePatterns.some(e => filename.match(e)))
|
||||
continue;
|
||||
if (await this.plugin.isIgnoredByIgnoreFiles(filename)) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileOnStorage = filename in filesMap ? filesMap[filename] : undefined;
|
||||
@ -335,7 +349,6 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
|
||||
// When files has been retrieved from the database. they must be reloaded.
|
||||
if ((direction == "pull" || direction == "pullForce") && filesChanged != 0) {
|
||||
const configDir = normalizePath(this.app.vault.configDir);
|
||||
// Show notification to restart obsidian when something has been changed in configDir.
|
||||
if (configDir in updatedFolders) {
|
||||
// Numbers of updated files that is below of configDir.
|
||||
@ -352,44 +365,18 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
updatedCount -= updatedFolders[manifest.dir];
|
||||
const updatePluginId = manifest.id;
|
||||
const updatePluginName = manifest.name;
|
||||
const fragment = createFragment((doc) => {
|
||||
doc.createEl("span", null, (a) => {
|
||||
a.appendText(`Files in ${updatePluginName} has been updated, Press `);
|
||||
a.appendChild(a.createEl("a", null, (anchor) => {
|
||||
anchor.text = "HERE";
|
||||
anchor.addEventListener("click", async () => {
|
||||
Logger(`Unloading plugin: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId);
|
||||
// @ts-ignore
|
||||
await this.app.plugins.unloadPlugin(updatePluginId);
|
||||
// @ts-ignore
|
||||
await this.app.plugins.loadPlugin(updatePluginId);
|
||||
Logger(`Plugin reloaded: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId);
|
||||
});
|
||||
}));
|
||||
|
||||
a.appendText(` to reload ${updatePluginName}, or press elsewhere to dismiss this message.`);
|
||||
this.plugin.askInPopup(`updated-${updatePluginId}`, `Files in ${updatePluginName} has been updated, Press {HERE} to reload ${updatePluginName}, or press elsewhere to dismiss this message.`, (anchor) => {
|
||||
anchor.text = "HERE";
|
||||
anchor.addEventListener("click", async () => {
|
||||
Logger(`Unloading plugin: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId);
|
||||
// @ts-ignore
|
||||
await this.app.plugins.unloadPlugin(updatePluginId);
|
||||
// @ts-ignore
|
||||
await this.app.plugins.loadPlugin(updatePluginId);
|
||||
Logger(`Plugin reloaded: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId);
|
||||
});
|
||||
});
|
||||
|
||||
const updatedPluginKey = "popupUpdated-" + updatePluginId;
|
||||
scheduleTask(updatedPluginKey, 1000, async () => {
|
||||
const popup = await memoIfNotExist(updatedPluginKey, () => new Notice(fragment, 0));
|
||||
//@ts-ignore
|
||||
const isShown = popup?.noticeEl?.isShown();
|
||||
if (!isShown) {
|
||||
memoObject(updatedPluginKey, new Notice(fragment, 0));
|
||||
}
|
||||
scheduleTask(updatedPluginKey + "-close", 20000, () => {
|
||||
const popup = retrieveMemoObject<Notice>(updatedPluginKey);
|
||||
if (!popup)
|
||||
return;
|
||||
//@ts-ignore
|
||||
if (popup?.noticeEl?.isShown()) {
|
||||
popup.hide();
|
||||
}
|
||||
disposeMemoObject(updatedPluginKey);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
@ -400,30 +387,11 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
|
||||
// If something changes left, notify for reloading Obsidian.
|
||||
if (updatedCount != 0) {
|
||||
const fragment = createFragment((doc) => {
|
||||
doc.createEl("span", null, (a) => {
|
||||
a.appendText(`Hidden files have been synchronized, Press `);
|
||||
a.appendChild(a.createEl("a", null, (anchor) => {
|
||||
anchor.text = "HERE";
|
||||
anchor.addEventListener("click", () => {
|
||||
// @ts-ignore
|
||||
this.app.commands.executeCommandById("app:reload");
|
||||
});
|
||||
}));
|
||||
|
||||
a.appendText(` to reload obsidian, or press elsewhere to dismiss this message.`);
|
||||
});
|
||||
});
|
||||
|
||||
scheduleTask("popupUpdated-" + configDir, 1000, () => {
|
||||
//@ts-ignore
|
||||
const isShown = this.confirmPopup?.noticeEl?.isShown();
|
||||
if (!isShown) {
|
||||
this.confirmPopup = new Notice(fragment, 0);
|
||||
}
|
||||
scheduleTask("popupClose" + configDir, 20000, () => {
|
||||
this.confirmPopup?.hide();
|
||||
this.confirmPopup = null;
|
||||
this.plugin.askInPopup(`updated-any-hidden`, `Hidden files have been synchronized, Press {HERE} to reload Obsidian, or press elsewhere to dismiss this message.`, (anchor) => {
|
||||
anchor.text = "HERE";
|
||||
anchor.addEventListener("click", () => {
|
||||
// @ts-ignore
|
||||
this.app.commands.executeCommandById("app:reload");
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -437,6 +405,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
if (await this.plugin.isIgnoredByIgnoreFiles(file.path)) {
|
||||
return
|
||||
}
|
||||
|
||||
const id = await this.path2id(file.path, ICHeader);
|
||||
const prefixedFileName = addPrefix(file.path, ICHeader);
|
||||
const contentBin = await this.app.vault.adapter.readBinary(file.path);
|
||||
@ -449,7 +418,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
return false;
|
||||
}
|
||||
const mtime = file.mtime;
|
||||
return await runWithLock("file-" + prefixedFileName, false, async () => {
|
||||
return await serialized("file-" + prefixedFileName, async () => {
|
||||
try {
|
||||
const old = await this.localDatabase.getDBEntry(prefixedFileName, null, false, false);
|
||||
let saveData: LoadedEntry;
|
||||
@ -501,7 +470,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
if (await this.plugin.isIgnoredByIgnoreFiles(filename)) {
|
||||
return
|
||||
}
|
||||
await runWithLock("file-" + prefixedFileName, false, async () => {
|
||||
await serialized("file-" + prefixedFileName, async () => {
|
||||
try {
|
||||
const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, null, true) as InternalFileEntry | false;
|
||||
let saveData: InternalFileEntry;
|
||||
@ -547,7 +516,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
if (await this.plugin.isIgnoredByIgnoreFiles(filename)) {
|
||||
return;
|
||||
}
|
||||
return await runWithLock("file-" + prefixedFileName, false, async () => {
|
||||
return await serialized("file-" + prefixedFileName, async () => {
|
||||
try {
|
||||
// Check conflicted status
|
||||
//TODO option
|
||||
@ -618,7 +587,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
|
||||
|
||||
showJSONMergeDialogAndMerge(docA: LoadedEntry, docB: LoadedEntry): Promise<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);
|
||||
const docs = [docA, docB];
|
||||
const path = stripAllPrefixes(docA.path);
|
||||
@ -676,13 +645,16 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
}
|
||||
|
||||
async scanInternalFiles(): Promise<InternalFileInfo[]> {
|
||||
const configDir = normalizePath(this.app.vault.configDir);
|
||||
const ignoreFilter = this.settings.syncInternalFilesIgnorePatterns
|
||||
.replace(/\n| /g, "")
|
||||
.split(",").filter(e => e).map(e => new RegExp(e, "i"));
|
||||
const synchronisedInConfigSync = !this.settings.usePluginSync ? [] : Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode == MODE_SELECTIVE || e.mode == MODE_PAUSED).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase());
|
||||
const root = this.app.vault.getRoot();
|
||||
const findRoot = root.path;
|
||||
|
||||
const filenames = (await this.getFiles(findRoot, [], null, ignoreFilter)).filter(e => e.startsWith(".")).filter(e => !e.startsWith(".trash"));
|
||||
const files = filenames.map(async (e) => {
|
||||
const files = filenames.filter(path => synchronisedInConfigSync.every(filterFile => !path.toLowerCase().startsWith(filterFile))).map(async (e) => {
|
||||
return {
|
||||
path: e as FilePath,
|
||||
stat: await this.app.vault.adapter.stat(e)
|
||||
@ -716,7 +688,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
...w.files
|
||||
.filter((e) => !ignoreList.some((ee) => e.endsWith(ee)))
|
||||
.filter((e) => !filter || filter.some((ee) => e.match(ee)))
|
||||
.filter((e) => !ignoreFilter || ignoreFilter.every((ee) => !e.match(ee))),
|
||||
.filter((e) => !ignoreFilter || ignoreFilter.every((ee) => !e.match(ee)))
|
||||
];
|
||||
let files = [] as string[];
|
||||
for (const file of filesSrc) {
|
||||
|
@ -9,7 +9,7 @@ import { isPluginMetadata, PeriodicProcessor } from "./utils";
|
||||
import { PluginDialogModal } from "./dialogs";
|
||||
import { NewNotice } from "./lib/src/wrapper";
|
||||
import { versionNumberString2Number } from "./lib/src/strbin";
|
||||
import { runWithLock } from "./lib/src/lock";
|
||||
import { serialized, skipIfDuplicated } from "./lib/src/lock";
|
||||
import { LiveSyncCommands } from "./LiveSyncCommands";
|
||||
|
||||
export class PluginAndTheirSettings extends LiveSyncCommands {
|
||||
@ -164,7 +164,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
||||
if (specificPluginPath != "") {
|
||||
specificPlugin = manifests.find(e => e.dir.endsWith("/" + specificPluginPath))?.id ?? "";
|
||||
}
|
||||
await runWithLock("sweepplugin", true, async () => {
|
||||
await skipIfDuplicated("sweepplugin", async () => {
|
||||
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||
if (!this.deviceAndVaultName) {
|
||||
Logger("You have to set your device name.", LOG_LEVEL_NOTICE);
|
||||
@ -223,7 +223,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
||||
type: "plain"
|
||||
};
|
||||
Logger(`check diff:${m.name}(${m.id})`, LOG_LEVEL_VERBOSE);
|
||||
await runWithLock("plugin-" + m.id, false, async () => {
|
||||
await serialized("plugin-" + m.id, async () => {
|
||||
const old = await this.localDatabase.getDBEntry(p._id as string as FilePathWithPrefix /* This also should be explained */, null, false, false);
|
||||
if (old !== false) {
|
||||
const oldData = { data: old.data, deleted: old._deleted };
|
||||
@ -266,7 +266,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
||||
}
|
||||
|
||||
async applyPluginData(plugin: PluginDataEntry) {
|
||||
await runWithLock("plugin-" + plugin.manifest.id, false, async () => {
|
||||
await serialized("plugin-" + plugin.manifest.id, async () => {
|
||||
const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/";
|
||||
const adapter = this.app.vault.adapter;
|
||||
// @ts-ignore
|
||||
@ -288,7 +288,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
||||
}
|
||||
|
||||
async applyPlugin(plugin: PluginDataEntry) {
|
||||
await runWithLock("plugin-" + plugin.manifest.id, false, async () => {
|
||||
await serialized("plugin-" + plugin.manifest.id, async () => {
|
||||
// @ts-ignore
|
||||
const stat = this.app.plugins.enabledPlugins.has(plugin.manifest.id) == true;
|
||||
if (stat) {
|
||||
|
@ -20,6 +20,11 @@ export class SetupLiveSync extends LiveSyncCommands {
|
||||
name: "Copy the setup URI",
|
||||
callback: this.command_copySetupURI.bind(this),
|
||||
});
|
||||
this.plugin.addCommand({
|
||||
id: "livesync-copysetupuri-short",
|
||||
name: "Copy the setup URI (With customization sync)",
|
||||
callback: this.command_copySetupURIWithSync.bind(this),
|
||||
});
|
||||
|
||||
this.plugin.addCommand({
|
||||
id: "livesync-copysetupurifull",
|
||||
@ -41,11 +46,14 @@ export class SetupLiveSync extends LiveSyncCommands {
|
||||
}
|
||||
async realizeSettingSyncMode() { }
|
||||
|
||||
async command_copySetupURI() {
|
||||
async command_copySetupURI(stripExtra = true) {
|
||||
const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "The passphrase to encrypt the setup URI", "", true);
|
||||
if (encryptingPassphrase === false)
|
||||
return;
|
||||
const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" };
|
||||
if (stripExtra) {
|
||||
delete setting.pluginSyncExtendedSetting;
|
||||
}
|
||||
const keys = Object.keys(setting) as (keyof ObsidianLiveSyncSettings)[];
|
||||
for (const k of keys) {
|
||||
if (JSON.stringify(k in setting ? setting[k] : "") == JSON.stringify(k in DEFAULT_SETTINGS ? DEFAULT_SETTINGS[k] : "*")) {
|
||||
@ -67,6 +75,9 @@ export class SetupLiveSync extends LiveSyncCommands {
|
||||
await navigator.clipboard.writeText(uri);
|
||||
Logger("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
async command_copySetupURIWithSync() {
|
||||
this.command_copySetupURI(false);
|
||||
}
|
||||
async command_openSetupURI() {
|
||||
const setupURI = await askString(this.app, "Easy setup", "Set up URI", `${configURIBase}aaaaa`);
|
||||
if (setupURI === false)
|
||||
@ -290,6 +301,7 @@ Of course, we are able to disable these features.`
|
||||
this.plugin.settings.liveSync = false;
|
||||
this.plugin.settings.periodicReplication = false;
|
||||
this.plugin.settings.syncOnSave = false;
|
||||
this.plugin.settings.syncOnEditorSave = false;
|
||||
this.plugin.settings.syncOnStart = false;
|
||||
this.plugin.settings.syncOnFileOpen = false;
|
||||
this.plugin.settings.syncAfterMerge = false;
|
||||
|
@ -18,10 +18,8 @@ export class ConflictResolveModal extends Modal {
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
|
||||
this.titleEl.setText("Conflicting changes");
|
||||
contentEl.empty();
|
||||
|
||||
contentEl.createEl("h2", { text: "This document has conflicted changes." });
|
||||
contentEl.createEl("span", { text: this.filename });
|
||||
const div = contentEl.createDiv("");
|
||||
div.addClass("op-scrollable");
|
||||
|
@ -10,29 +10,29 @@ import { stripPrefix } from "./lib/src/path";
|
||||
|
||||
export class DocumentHistoryModal extends Modal {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
range: HTMLInputElement;
|
||||
contentView: HTMLDivElement;
|
||||
info: HTMLDivElement;
|
||||
fileInfo: HTMLDivElement;
|
||||
range!: HTMLInputElement;
|
||||
contentView!: HTMLDivElement;
|
||||
info!: HTMLDivElement;
|
||||
fileInfo!: HTMLDivElement;
|
||||
showDiff = false;
|
||||
id: DocumentID;
|
||||
id?: DocumentID;
|
||||
|
||||
file: FilePathWithPrefix;
|
||||
|
||||
revs_info: PouchDB.Core.RevisionInfo[] = [];
|
||||
currentDoc: LoadedEntry;
|
||||
currentDoc?: LoadedEntry;
|
||||
currentText = "";
|
||||
currentDeleted = false;
|
||||
initialRev: string;
|
||||
initialRev?: string;
|
||||
|
||||
constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile | FilePathWithPrefix, id: DocumentID, revision?: string) {
|
||||
constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile | FilePathWithPrefix, id?: DocumentID, revision?: string) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.file = (file instanceof TFile) ? getPathFromTFile(file) : file;
|
||||
this.id = id;
|
||||
this.initialRev = revision;
|
||||
if (!file) {
|
||||
this.file = this.plugin.id2path(id, null);
|
||||
if (!file && id) {
|
||||
this.file = this.plugin.id2path(id);
|
||||
}
|
||||
if (localStorage.getItem("ols-history-highlightdiff") == "1") {
|
||||
this.showDiff = true;
|
||||
@ -46,8 +46,8 @@ export class DocumentHistoryModal extends Modal {
|
||||
const db = this.plugin.localDatabase;
|
||||
try {
|
||||
const w = await db.localDatabase.get(this.id, { revs_info: true });
|
||||
this.revs_info = w._revs_info.filter((e) => e?.status == "available");
|
||||
this.range.max = `${this.revs_info.length - 1}`;
|
||||
this.revs_info = w._revs_info?.filter((e) => e?.status == "available") ?? [];
|
||||
this.range.max = `${Math.max(this.revs_info.length - 1, 0)}`;
|
||||
this.range.value = this.range.max;
|
||||
this.fileInfo.setText(`${this.file} / ${this.revs_info.length} revisions`);
|
||||
await this.loadRevs(initialRev);
|
||||
@ -90,7 +90,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
|
||||
let result = "";
|
||||
const w1data = w.datatype == "plain" ? getDocData(w.data) : base64ToString(w.data);
|
||||
this.currentDeleted = w.deleted;
|
||||
this.currentDeleted = !!w.deleted;
|
||||
this.currentText = w1data;
|
||||
if (this.showDiff) {
|
||||
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
|
||||
@ -130,9 +130,8 @@ export class DocumentHistoryModal extends Modal {
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
|
||||
this.titleEl.setText("Document History");
|
||||
contentEl.empty();
|
||||
contentEl.createEl("h2", { text: "Document History" });
|
||||
this.fileInfo = contentEl.createDiv("");
|
||||
this.fileInfo.addClass("op-info");
|
||||
const divView = contentEl.createDiv("");
|
||||
|
@ -29,7 +29,7 @@ export class JsonResolveModal extends Modal {
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
|
||||
this.titleEl.setText("Conflicted Setting");
|
||||
contentEl.empty();
|
||||
|
||||
if (this.component == null) {
|
||||
|
@ -104,7 +104,6 @@
|
||||
] as ["" | "A" | "B" | "AB" | "BA", string][];
|
||||
</script>
|
||||
|
||||
<h1>Conflicted settings</h1>
|
||||
<h2>{filename}</h2>
|
||||
{#if !docA || !docB}
|
||||
<div class="message">Just for a minute, please!</div>
|
||||
|
@ -14,9 +14,9 @@ export class LogDisplayModal extends Modal {
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
this.titleEl.setText("Sync status");
|
||||
|
||||
contentEl.empty();
|
||||
contentEl.createEl("h2", { text: "Sync Status" });
|
||||
const div = contentEl.createDiv("");
|
||||
div.addClass("op-scrollable");
|
||||
div.addClass("op-pre");
|
||||
|
@ -120,6 +120,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
if (this.plugin.settings.periodicReplication) return true;
|
||||
if (this.plugin.settings.syncOnFileOpen) return true;
|
||||
if (this.plugin.settings.syncOnSave) return true;
|
||||
if (this.plugin.settings.syncOnEditorSave) return true;
|
||||
if (this.plugin.settings.syncOnStart) return true;
|
||||
if (this.plugin.settings.syncAfterMerge) return true;
|
||||
if (this.plugin.replicator.syncStatus == "CONNECTED") return true;
|
||||
@ -157,6 +158,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
this.plugin.settings.liveSync = false;
|
||||
this.plugin.settings.periodicReplication = false;
|
||||
this.plugin.settings.syncOnSave = false;
|
||||
this.plugin.settings.syncOnEditorSave = false;
|
||||
this.plugin.settings.syncOnStart = false;
|
||||
this.plugin.settings.syncOnFileOpen = false;
|
||||
this.plugin.settings.syncAfterMerge = false;
|
||||
@ -216,7 +218,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
syncLive.forEach((e) => {
|
||||
e.setDisabled(false).setTooltip("");
|
||||
});
|
||||
} else if (this.plugin.settings.syncOnFileOpen || this.plugin.settings.syncOnSave || this.plugin.settings.syncOnStart || this.plugin.settings.periodicReplication || this.plugin.settings.syncAfterMerge) {
|
||||
} else if (this.plugin.settings.syncOnFileOpen || this.plugin.settings.syncOnSave || this.plugin.settings.syncOnEditorSave || this.plugin.settings.syncOnStart || this.plugin.settings.periodicReplication || this.plugin.settings.syncAfterMerge) {
|
||||
syncNonLive.forEach((e) => {
|
||||
e.setDisabled(false).setTooltip("");
|
||||
});
|
||||
@ -891,6 +893,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
liveSync: false,
|
||||
periodicReplication: false,
|
||||
syncOnSave: false,
|
||||
syncOnEditorSave: false,
|
||||
syncOnStart: false,
|
||||
syncOnFileOpen: false,
|
||||
syncAfterMerge: false,
|
||||
@ -904,6 +907,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
batchSave: true,
|
||||
periodicReplication: true,
|
||||
syncOnSave: false,
|
||||
syncOnEditorSave: false,
|
||||
syncOnStart: true,
|
||||
syncOnFileOpen: true,
|
||||
syncAfterMerge: true,
|
||||
@ -1014,6 +1018,17 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
applyDisplayEnabled();
|
||||
})
|
||||
)
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Sync on Editor Save")
|
||||
.setDesc("When you save file on the editor, sync automatically")
|
||||
.setClass("wizardHidden")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.syncOnEditorSave).onChange(async (value) => {
|
||||
this.plugin.settings.syncOnEditorSave = value;
|
||||
await this.plugin.saveSettings();
|
||||
applyDisplayEnabled();
|
||||
})
|
||||
)
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Sync on File Open")
|
||||
.setDesc("When you open file, sync automatically")
|
||||
@ -1199,7 +1214,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, \\/obsidian-livesync\\/";
|
||||
const defaultSkipPatternXPlat = defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$";
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Skip patterns")
|
||||
.setName("Folders and files to ignore")
|
||||
.setDesc(
|
||||
"Regular expression, If you use hidden file sync between desktop and mobile, adding `workspace$` is recommended."
|
||||
)
|
||||
@ -1777,8 +1792,8 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
dropdown
|
||||
.addOptions({ "": "Old Algorithm", "xxhash32": "xxhash32 (Fast)", "xxhash64": "xxhash64 (Fastest)" } as Record<HashAlgorithm, string>)
|
||||
.setValue(this.plugin.settings.hashAlg)
|
||||
.onChange(async (value: HashAlgorithm) => {
|
||||
this.plugin.settings.hashAlg = value;
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hashAlg = value as HashAlgorithm;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
)
|
||||
|
@ -108,7 +108,7 @@
|
||||
}
|
||||
}
|
||||
})
|
||||
.reduce((p, c) => p | c, 0);
|
||||
.reduce((p, c) => p | (c as number), 0 as number);
|
||||
if (matchingStatus == 0b0000100) {
|
||||
equivalency = "⚖️ Same";
|
||||
canApply = false;
|
||||
|
@ -3,9 +3,13 @@
|
||||
import ObsidianLiveSyncPlugin from "./main";
|
||||
import { type PluginDataExDisplay, pluginIsEnumerating, pluginList } from "./CmdConfigSync";
|
||||
import PluginCombo from "./PluginCombo.svelte";
|
||||
import { Menu } from "obsidian";
|
||||
import { unique } from "./lib/src/utils";
|
||||
import { MODE_SELECTIVE, MODE_AUTOMATIC, MODE_PAUSED, type SYNC_MODE, type PluginSyncSettingEntry } from "./lib/src/types";
|
||||
import { normalizePath } from "./deps";
|
||||
export let plugin: ObsidianLiveSyncPlugin;
|
||||
|
||||
$: hideNotApplicable = true;
|
||||
$: hideNotApplicable = false;
|
||||
$: thisTerm = plugin.deviceAndVaultName;
|
||||
|
||||
const addOn = plugin.addOnConfigSync;
|
||||
@ -13,7 +17,7 @@
|
||||
let list: PluginDataExDisplay[] = [];
|
||||
|
||||
let selectNewestPulse = 0;
|
||||
let hideEven = true;
|
||||
let hideEven = false;
|
||||
let loading = false;
|
||||
let applyAllPluse = 0;
|
||||
let isMaintenanceMode = false;
|
||||
@ -80,6 +84,54 @@
|
||||
async function deleteData(data: PluginDataExDisplay): Promise<boolean> {
|
||||
return await addOn.deleteData(data);
|
||||
}
|
||||
function askMode(evt: MouseEvent, title: string, key: string) {
|
||||
const menu = new Menu();
|
||||
menu.addItem((item) => item.setTitle(title).setIsLabel(true));
|
||||
menu.addSeparator();
|
||||
const prevMode = automaticList.get(key) ?? MODE_SELECTIVE;
|
||||
for (const mode of [MODE_SELECTIVE, MODE_AUTOMATIC, MODE_PAUSED]) {
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(`${getIcon(mode as SYNC_MODE)}:${TITLES[mode]}`)
|
||||
.onClick((e) => {
|
||||
if (mode === MODE_AUTOMATIC) {
|
||||
askOverwriteModeForAutomatic(evt, key);
|
||||
} else {
|
||||
setMode(key, mode as SYNC_MODE);
|
||||
}
|
||||
})
|
||||
.setChecked(prevMode == mode)
|
||||
.setDisabled(prevMode == mode);
|
||||
});
|
||||
}
|
||||
menu.showAtMouseEvent(evt);
|
||||
}
|
||||
function applyAutomaticSync(key: string, direction: "pushForce" | "pullForce" | "safe") {
|
||||
setMode(key, MODE_AUTOMATIC);
|
||||
const configDir = normalizePath(plugin.app.vault.configDir);
|
||||
const files = (plugin.settings.pluginSyncExtendedSetting[key]?.files ?? []).map((e) => `${configDir}/${e}`);
|
||||
plugin.addOnHiddenFileSync.syncInternalFilesAndDatabase(direction, true, false, files);
|
||||
}
|
||||
function askOverwriteModeForAutomatic(evt: MouseEvent, key: string) {
|
||||
const menu = new Menu();
|
||||
menu.addItem((item) => item.setTitle("Initial Action").setIsLabel(true));
|
||||
menu.addSeparator();
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(`↑: Overwrite Remote`).onClick((e) => {
|
||||
applyAutomaticSync(key, "pushForce");
|
||||
});
|
||||
})
|
||||
.addItem((item) => {
|
||||
item.setTitle(`↓: Overwrite Local`).onClick((e) => {
|
||||
applyAutomaticSync(key, "pullForce");
|
||||
});
|
||||
})
|
||||
.addItem((item) => {
|
||||
item.setTitle(`⇅: Use newer`).onClick((e) => {
|
||||
applyAutomaticSync(key, "safe");
|
||||
});
|
||||
});
|
||||
menu.showAtMouseEvent(evt);
|
||||
}
|
||||
|
||||
$: options = {
|
||||
thisTerm,
|
||||
@ -92,11 +144,84 @@
|
||||
plugin,
|
||||
isMaintenanceMode,
|
||||
};
|
||||
|
||||
const ICON_EMOJI_PAUSED = `⛔`;
|
||||
const ICON_EMOJI_AUTOMATIC = `✨`;
|
||||
const ICON_EMOJI_SELECTIVE = `🔀`;
|
||||
|
||||
const ICONS: { [key: number]: string } = {
|
||||
[MODE_SELECTIVE]: ICON_EMOJI_SELECTIVE,
|
||||
[MODE_PAUSED]: ICON_EMOJI_PAUSED,
|
||||
[MODE_AUTOMATIC]: ICON_EMOJI_AUTOMATIC,
|
||||
};
|
||||
const TITLES: { [key: number]: string } = {
|
||||
[MODE_SELECTIVE]: "Selective",
|
||||
[MODE_PAUSED]: "Ignore",
|
||||
[MODE_AUTOMATIC]: "Automatic",
|
||||
};
|
||||
const PREFIX_PLUGIN_ALL = "PLUGIN_ALL";
|
||||
const PREFIX_PLUGIN_DATA = "PLUGIN_DATA";
|
||||
const PREFIX_PLUGIN_MAIN = "PLUGIN_MAIN";
|
||||
function setMode(key: string, mode: SYNC_MODE) {
|
||||
if (key.startsWith(PREFIX_PLUGIN_ALL + "/")) {
|
||||
setMode(PREFIX_PLUGIN_DATA + key.substring(PREFIX_PLUGIN_ALL.length), mode);
|
||||
setMode(PREFIX_PLUGIN_MAIN + key.substring(PREFIX_PLUGIN_ALL.length), mode);
|
||||
}
|
||||
const files = unique(
|
||||
list
|
||||
.filter((e) => `${e.category}/${e.name}` == key)
|
||||
.map((e) => e.files)
|
||||
.flat()
|
||||
.map((e) => e.filename)
|
||||
);
|
||||
automaticList.set(key, mode);
|
||||
automaticListDisp = automaticList;
|
||||
if (!(key in plugin.settings.pluginSyncExtendedSetting)) {
|
||||
plugin.settings.pluginSyncExtendedSetting[key] = {
|
||||
key,
|
||||
mode,
|
||||
files: [],
|
||||
};
|
||||
}
|
||||
plugin.settings.pluginSyncExtendedSetting[key].files = files;
|
||||
plugin.settings.pluginSyncExtendedSetting[key].mode = mode;
|
||||
plugin.saveSettingData();
|
||||
}
|
||||
function getIcon(mode: SYNC_MODE) {
|
||||
if (mode in ICONS) {
|
||||
return ICONS[mode];
|
||||
} else {
|
||||
("");
|
||||
}
|
||||
}
|
||||
let automaticList = new Map<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>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<h1>Customization sync</h1>
|
||||
<div class="buttons">
|
||||
<button on:click={() => scanAgain()}>Scan changes</button>
|
||||
<button on:click={() => replicate()}>Sync once</button>
|
||||
@ -119,15 +244,24 @@
|
||||
{#if list.length == 0}
|
||||
<div class="center">No Items.</div>
|
||||
{:else}
|
||||
{#each Object.entries(displays) as [key, label]}
|
||||
{#each Object.entries(displays).filter(([key, _]) => key in displayKeys) as [key, label]}
|
||||
<div>
|
||||
<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="title">
|
||||
{name}
|
||||
<button class="status" on:click={(evt) => askMode(evt, `${key}/${name}`, bindKey)}>
|
||||
{getIcon(mode)}
|
||||
</button>
|
||||
<span class="name">{name}</span>
|
||||
</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>
|
||||
{/each}
|
||||
</div>
|
||||
@ -135,20 +269,55 @@
|
||||
<div>
|
||||
<h3>Plugins</h3>
|
||||
{#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="title">
|
||||
{name}
|
||||
<button class="status" on:click={(evt) => askMode(evt, `${PREFIX_PLUGIN_ALL}/${name}`, bindKeyAll)}>
|
||||
{getIcon(modeAll)}
|
||||
</button>
|
||||
<span class="name">{name}</span>
|
||||
</div>
|
||||
<PluginCombo {...options} list={listX} hidden={true} />
|
||||
</div>
|
||||
<div class="filerow {hideEven ? 'hideeven' : ''}">
|
||||
<div class="filetitle">Main</div>
|
||||
<PluginCombo {...options} list={filterList(listX, ["PLUGIN_MAIN"])} hidden={false} />
|
||||
</div>
|
||||
<div class="filerow {hideEven ? 'hideeven' : ''}">
|
||||
<div class="filetitle">Data</div>
|
||||
<PluginCombo {...options} list={filterList(listX, ["PLUGIN_DATA"])} hidden={false} />
|
||||
{#if modeAll == MODE_SELECTIVE}
|
||||
<PluginCombo {...options} list={listX} hidden={true} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if modeAll == MODE_SELECTIVE}
|
||||
<div class="filerow {hideEven ? 'hideeven' : ''}">
|
||||
<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} />
|
||||
{:else}
|
||||
<div class="statusnote">{TITLES[modeMain]}</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="filerow {hideEven ? 'hideeven' : ''}">
|
||||
<div class="filetitle">
|
||||
<button class="status" on:click={(evt) => askMode(evt, `${PREFIX_PLUGIN_DATA}/${name}`, bindKeyData)}>
|
||||
{getIcon(modeData)}
|
||||
</button>
|
||||
<span class="name">DATA</span>
|
||||
</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}
|
||||
</div>
|
||||
{/if}
|
||||
@ -162,6 +331,15 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
span.spacer {
|
||||
min-width: 1px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
h3 {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--modal-background);
|
||||
}
|
||||
.labelrow {
|
||||
margin-left: 0.4em;
|
||||
display: flex;
|
||||
@ -183,6 +361,24 @@
|
||||
.labelrow.hideeven:has(.even) {
|
||||
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 {
|
||||
color: var(--text-normal);
|
||||
|
@ -18,7 +18,7 @@ type LiveSyncForStorageEventManager = Plugin &
|
||||
ignoreFiles: string[],
|
||||
} & {
|
||||
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,
|
||||
parseYaml, ItemView, WorkspaceLeaf
|
||||
} from "obsidian";
|
||||
export type { DataWriteOptions, PluginManifest, RequestUrlParam, RequestUrlResponse } from "obsidian";
|
||||
export type { DataWriteOptions, PluginManifest, RequestUrlParam, RequestUrlResponse, MarkdownFileInfo } from "obsidian";
|
||||
import {
|
||||
normalizePath as normalizePath_
|
||||
} from "obsidian";
|
||||
|
@ -20,6 +20,7 @@ export class PluginDialogModal extends Modal {
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
this.titleEl.setText("Customization Sync (Beta2)")
|
||||
if (this.component == null) {
|
||||
this.component = new PluginPane({
|
||||
target: contentEl,
|
||||
@ -38,7 +39,7 @@ export class PluginDialogModal extends Modal {
|
||||
|
||||
export class InputStringDialog extends Modal {
|
||||
result: string | false = false;
|
||||
onSubmit: (result: string | boolean) => void;
|
||||
onSubmit: (result: string | false) => void;
|
||||
title: string;
|
||||
key: string;
|
||||
placeholder: string;
|
||||
@ -56,8 +57,7 @@ export class InputStringDialog extends Modal {
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
|
||||
contentEl.createEl("h1", { text: this.title });
|
||||
this.titleEl.setText(this.title);
|
||||
// For enter to submit
|
||||
const formEl = contentEl.createEl("form");
|
||||
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;
|
||||
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) {
|
||||
super(plugin.app);
|
||||
@ -175,6 +175,7 @@ export class MessageBox extends Modal {
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
this.titleEl.setText(this.title);
|
||||
contentEl.addEventListener("click", () => {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
|
2
src/lib
2
src/lib
@ -1 +1 @@
|
||||
Subproject commit 70eb916288d0f62df40dd4fe4341afb31e4730a3
|
||||
Subproject commit 6548bd3ed75dc087d74acdac5a473f95aaccf6a9
|
243
src/main.ts
243
src/main.ts
@ -1,7 +1,7 @@
|
||||
const isDebug = false;
|
||||
|
||||
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 InternalFileInfo, type queueItem, type CacheData, type FileEventItem, FileWatchEventQueueMax } from "./types";
|
||||
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 { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
||||
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
||||
import { applyPatch, cancelAllPeriodicTask, cancelAllTasks, cancelTask, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, flattenObject, path2id, scheduleTask, tryParseJSON, createFile, modifyFile, isValidPath, getAbstractFileByPath, touch, recentlyTouched, 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 { balanceChunkPurgedDBs, enableEncryption, isCloudantURI, isErrorOfMissingDoc, isValidRemoteCouchDBURI, purgeUnreferencedChunks } from "./lib/src/utils_couchdb";
|
||||
import { getGlobalStore, ObservableStore, observeStores } from "./lib/src/store";
|
||||
import { lockStore, logMessageStore, logStore, type LogEntry } from "./lib/src/stores";
|
||||
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 { isLockAcquired, runWithLock } from "./lib/src/lock";
|
||||
import { isLockAcquired, serialized, skipIfDuplicated } from "./lib/src/lock";
|
||||
import { Semaphore } from "./lib/src/semaphore";
|
||||
import { StorageEventManager, StorageEventManagerObsidian } from "./StorageEventManager";
|
||||
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)));
|
||||
|
||||
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
|
||||
implements LiveSyncLocalDBEnv, LiveSyncReplicatorEnv {
|
||||
|
||||
settings: ObsidianLiveSyncSettings;
|
||||
localDatabase: LiveSyncLocalDB;
|
||||
replicator: LiveSyncDBReplicator;
|
||||
settings!: ObsidianLiveSyncSettings;
|
||||
localDatabase!: LiveSyncLocalDB;
|
||||
replicator!: LiveSyncDBReplicator;
|
||||
|
||||
statusBar: HTMLElement;
|
||||
suspended: boolean;
|
||||
deviceAndVaultName: string;
|
||||
statusBar?: HTMLElement;
|
||||
suspended: boolean = true;
|
||||
deviceAndVaultName: string = "";
|
||||
isMobile = false;
|
||||
isReady = false;
|
||||
packageVersion = "";
|
||||
@ -67,25 +80,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
periodicSyncProcessor = new PeriodicProcessor(this, async () => await this.replicate());
|
||||
|
||||
// implementing interfaces
|
||||
kvDB: KeyValueDatabase;
|
||||
kvDB!: KeyValueDatabase;
|
||||
last_successful_post = false;
|
||||
getLastPostFailedBySize() {
|
||||
return !this.last_successful_post;
|
||||
}
|
||||
|
||||
async 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;
|
||||
}
|
||||
|
||||
_unloaded = false;
|
||||
|
||||
getDatabase(): PouchDB.Database<EntryDoc> {
|
||||
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.";
|
||||
let authHeader = "";
|
||||
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);
|
||||
authHeader = "Basic " + encoded;
|
||||
} else {
|
||||
@ -115,11 +118,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
adapter: "http",
|
||||
auth,
|
||||
skip_setup: !performSetup,
|
||||
fetch: async (url: string | Request, opts: RequestInit) => {
|
||||
fetch: async (url: string | Request, opts?: RequestInit) => {
|
||||
let size = "";
|
||||
const localURL = url.toString().substring(uri.length);
|
||||
const method = opts.method ?? "GET";
|
||||
if (opts.body) {
|
||||
const method = opts?.method ?? "GET";
|
||||
if (opts?.body) {
|
||||
const opts_length = opts.body.toString().length;
|
||||
if (opts_length > 1000 * 1000 * 10) {
|
||||
// over 10MB
|
||||
@ -132,10 +135,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
size = ` (${opts_length})`;
|
||||
}
|
||||
|
||||
if (!disableRequestURI && typeof url == "string" && typeof (opts.body ?? "") == "string") {
|
||||
const body = opts.body as string;
|
||||
if (!disableRequestURI && typeof url == "string" && typeof (opts?.body ?? "") == "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;
|
||||
delete transformedHeaders["host"];
|
||||
delete transformedHeaders["Host"];
|
||||
@ -143,7 +146,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
delete transformedHeaders["Content-Length"];
|
||||
const requestParam: RequestUrlParam = {
|
||||
url,
|
||||
method: opts.method,
|
||||
method: opts?.method,
|
||||
body: body,
|
||||
headers: transformedHeaders,
|
||||
contentType: "application/json",
|
||||
@ -151,7 +154,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
};
|
||||
|
||||
try {
|
||||
const r = await this.fetchByAPI(requestParam);
|
||||
const r = await fetchByAPI(requestParam);
|
||||
if (method == "POST" || method == "PUT") {
|
||||
this.last_successful_post = r.status - (r.status % 100) == 200;
|
||||
} else {
|
||||
@ -209,9 +212,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
try {
|
||||
const info = await db.info();
|
||||
return { db: db, info: info };
|
||||
} catch (ex) {
|
||||
let msg = `${ex.name}:${ex.message}`;
|
||||
if (ex.name == "TypeError" && ex.message == "Failed to fetch") {
|
||||
} catch (ex: any) {
|
||||
let msg = `${ex?.name}:${ex?.message}`;
|
||||
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.";
|
||||
}
|
||||
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);
|
||||
if (stripPrefix && isInternalMetadata(tempId)) {
|
||||
const out = stripInternalMetadataPrefix(tempId);
|
||||
@ -234,18 +237,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
return getPathWithoutPrefix(entry);
|
||||
}
|
||||
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 : "");
|
||||
}
|
||||
|
||||
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) {
|
||||
options.adapter = "indexeddb";
|
||||
optionPass.adapter = "indexeddb";
|
||||
//@ts-ignore :missing def
|
||||
options.purged_infos_limit = 1;
|
||||
return new PouchDB(name + "-indexeddb", options);
|
||||
optionPass.purged_infos_limit = 1;
|
||||
return new PouchDB(name + "-indexeddb", optionPass);
|
||||
}
|
||||
return new PouchDB(name, options);
|
||||
return new PouchDB(name, optionPass);
|
||||
}
|
||||
beforeOnUnload(db: LiveSyncLocalDB): void {
|
||||
this.kvDB.close();
|
||||
@ -258,6 +262,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
this.replicator = new LiveSyncDBReplicator(this);
|
||||
}
|
||||
async onResetDatabase(db: LiveSyncLocalDB): Promise<void> {
|
||||
const lsKey = "obsidian-livesync-queuefiles-" + this.getVaultName();
|
||||
localStorage.removeItem(lsKey);
|
||||
await this.kvDB.destroy();
|
||||
this.kvDB = await OpenKeyValueDatabase(db.dbname + "-livesync-kv");
|
||||
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();
|
||||
}
|
||||
|
||||
@ -334,7 +340,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
const target = await askSelectString(this.app, "File to view History", notesList);
|
||||
if (target) {
|
||||
const targetId = notes.find(e => e.dispPath == target);
|
||||
this.showHistory(targetId.path, undefined);
|
||||
this.showHistory(targetId.path, targetId.id);
|
||||
}
|
||||
}
|
||||
async pickFileForResolve() {
|
||||
@ -349,7 +355,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
Logger("There are no conflicted documents", LOG_LEVEL_NOTICE);
|
||||
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) {
|
||||
const targetItem = notes.find(e => e.dispPath == target);
|
||||
await this.resolveConflicted(targetItem.path);
|
||||
@ -363,6 +369,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
await this.addOnHiddenFileSync.resolveConflictOnInternalFile(target);
|
||||
} else if (isPluginMetadata(target)) {
|
||||
await this.resolveConflictByNewerEntry(target);
|
||||
} else if (isCustomisationSyncMetadata(target)) {
|
||||
await this.resolveConflictByNewerEntry(target);
|
||||
} else {
|
||||
await this.showIfConflicted(target);
|
||||
}
|
||||
@ -453,6 +461,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
}
|
||||
await this.realizeSettingSyncMode();
|
||||
this.registerWatchEvents();
|
||||
this.swapSaveCommand();
|
||||
if (this.settings.syncOnStart) {
|
||||
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 });
|
||||
}
|
||||
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) {
|
||||
Logger(`Conflicted: ${note.path}`);
|
||||
}
|
||||
@ -512,6 +529,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
if (last_version && Number(last_version) < VER) {
|
||||
this.settings.liveSync = false;
|
||||
this.settings.syncOnSave = false;
|
||||
this.settings.syncOnEditorSave = false;
|
||||
this.settings.syncOnStart = false;
|
||||
this.settings.syncOnFileOpen = false;
|
||||
this.settings.syncAfterMerge = false;
|
||||
@ -572,15 +590,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
this.addCommand({
|
||||
id: "livesync-dump",
|
||||
name: "Dump information of this doc ",
|
||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||
this.localDatabase.getDBEntry(getPathFromTFile(view.file), {}, true, false);
|
||||
editorCallback: (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
|
||||
const file = view.file;
|
||||
if (!file) return;
|
||||
this.localDatabase.getDBEntry(getPathFromTFile(file), {}, true, false);
|
||||
},
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-checkdoc-conflicted",
|
||||
name: "Resolve if conflicted.",
|
||||
editorCallback: async (editor: Editor, view: MarkdownView) => {
|
||||
await this.showIfConflicted(getPathFromTFile(view.file));
|
||||
editorCallback: async (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
|
||||
const file = view.file;
|
||||
if (!file) return;
|
||||
await this.showIfConflicted(getPathFromTFile(file));
|
||||
},
|
||||
});
|
||||
|
||||
@ -617,8 +639,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
this.addCommand({
|
||||
id: "livesync-history",
|
||||
name: "Show history",
|
||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||
this.showHistory(view.file, null);
|
||||
editorCallback: (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
|
||||
if (view.file) this.showHistory(view.file, null);
|
||||
},
|
||||
});
|
||||
this.addCommand({
|
||||
@ -720,6 +742,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
}
|
||||
cancelAllPeriodicTask();
|
||||
cancelAllTasks();
|
||||
this._unloaded = true;
|
||||
Logger("unloading plugin");
|
||||
}
|
||||
|
||||
@ -852,7 +875,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
(async () => await this.realizeSettingSyncMode())();
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
|
||||
async saveSettingData() {
|
||||
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.getVaultName();
|
||||
|
||||
localStorage.setItem(lsKey, this.deviceAndVaultName || "");
|
||||
@ -882,6 +906,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
await this.saveData(settings);
|
||||
this.localDatabase.settings = this.settings;
|
||||
this.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveSettingData();
|
||||
this.triggerRealizeSettingSyncMode();
|
||||
}
|
||||
|
||||
@ -889,7 +917,38 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
registerFileWatchEvents() {
|
||||
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() {
|
||||
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
|
||||
this.registerDomEvent(document, "visibilitychange", this.watchWindowVisibility);
|
||||
@ -949,7 +1008,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
}
|
||||
}
|
||||
cancelTask("applyBatchAuto");
|
||||
const ret = await runWithLock("procFiles", true, async () => {
|
||||
const ret = await skipIfDuplicated("procFiles", async () => {
|
||||
do {
|
||||
const queue = this.vaultManager.fetchEvent();
|
||||
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.isReady) return;
|
||||
if (!file) return;
|
||||
this.watchWorkspaceOpenAsync(file);
|
||||
}
|
||||
|
||||
@ -1269,7 +1329,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
const path = getPath(entry);
|
||||
try {
|
||||
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);
|
||||
await this.handleDBChangedAsync(entry);
|
||||
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.remoteCleaned && this.settings.useIndexedDBAdapter) {
|
||||
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 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.
|
||||
@ -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);
|
||||
return true;
|
||||
}
|
||||
// first, check for same contents and deletion status.
|
||||
if (leftLeaf.data == rightLeaf.data && leftLeaf.deleted == rightLeaf.deleted) {
|
||||
let leaf = leftLeaf;
|
||||
if (leftLeaf.mtime > rightLeaf.mtime) {
|
||||
leaf = rightLeaf;
|
||||
}
|
||||
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 isSame = leftLeaf.data == rightLeaf.data && leftLeaf.deleted == rightLeaf.deleted;
|
||||
const isBinary = !isPlainText(path);
|
||||
const alwaysNewer = this.settings.resolveConflictsByNewerFile;
|
||||
if (isSame || isBinary || alwaysNewer) {
|
||||
const lMtime = ~~(leftLeaf.mtime / 1000);
|
||||
const rMtime = ~~(rightLeaf.mtime / 1000);
|
||||
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.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;
|
||||
}
|
||||
// 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> {
|
||||
return runWithLock("resolve-conflict:" + filename, false, () =>
|
||||
return serialized("resolve-conflict:" + filename, () =>
|
||||
new Promise((res, rej) => {
|
||||
Logger("open conflict dialog", LOG_LEVEL_VERBOSE);
|
||||
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) {
|
||||
await runWithLock("conflicted", false, async () => {
|
||||
await serialized("conflicted", async () => {
|
||||
const conflictCheckResult = await this.getConflictedStatus(filename);
|
||||
if (conflictCheckResult === false) {
|
||||
//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
|
||||
const msg = `DB <- STORAGE (${datatype}) `;
|
||||
const isNotChanged = await runWithLock("file-" + fullPath, false, async () => {
|
||||
const isNotChanged = await serialized("file-" + fullPath, async () => {
|
||||
if (recentlyTouched(file)) {
|
||||
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);
|
||||
}
|
||||
async dryRunGC() {
|
||||
await runWithLock("cleanup", true, async () => {
|
||||
await skipIfDuplicated("cleanup", async () => {
|
||||
const remoteDBConn = await this.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.isMobile)
|
||||
if (typeof (remoteDBConn) == "string") {
|
||||
Logger(remoteDBConn);
|
||||
@ -2622,7 +2675,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
|
||||
async dbGC() {
|
||||
// Lock the remote completely once.
|
||||
await runWithLock("cleanup", true, async () => {
|
||||
await skipIfDuplicated("cleanup", async () => {
|
||||
this.getReplicator().markRemoteLocked(this.settings, true, true);
|
||||
const remoteDBConn = await this.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.isMobile)
|
||||
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.")
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
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 { 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 ObsidianLiveSyncPlugin from "./main";
|
||||
import { writeString } from "./lib/src/strbin";
|
||||
|
||||
// For backward compatibility, using the path for determining id.
|
||||
// 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 {
|
||||
return str.startsWith(PSCHeader);
|
||||
}
|
||||
export function isCustomisationSyncMetadata(str: string): boolean {
|
||||
return str.startsWith(ICXHeader);
|
||||
}
|
||||
|
||||
export const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => {
|
||||
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) => {
|
||||
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 authHeader = "Basic " + encoded;
|
||||
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) => {
|
||||
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 authHeader = "Basic " + encoded;
|
||||
const transformedHeaders: Record<string, string> = { authorization: authHeader, origin: origin };
|
||||
|
Loading…
x
Reference in New Issue
Block a user