Compare commits

...

4 Commits
0.5.0 ... 0.7.1

Author SHA1 Message Date
vorotamoroz
31ab0e90f6 Fixed:
- Device and vault name is now not stored in the data.json.
You can synchronize LiveSync's configuration!
2022-02-18 20:10:43 +09:00
vorotamoroz
43fba807c3 Implemented: New "plugins and their settings"
Fixed: some plugin synchronization bugs.
2022-02-16 18:26:13 +09:00
vorotamoroz
3a8e52425e Fixed:
- Some extensions are encoded incorrectly.
2022-01-27 12:15:23 +09:00
vorotamoroz
15b580aa9a Implemented:
- History dialog

Improved:
- Speed up Garbage Collection.
2022-01-13 17:41:45 +09:00
13 changed files with 1710 additions and 304 deletions

37
esbuild.config.mjs Normal file
View File

@@ -0,0 +1,37 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import sveltePlugin from "esbuild-svelte";
import sveltePreprocess from "svelte-preprocess";
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = process.argv[2] === "production";
esbuild
.build({
banner: {
js: banner,
},
entryPoints: ["src/main.ts"],
bundle: true,
external: ["obsidian", "electron", ...builtins],
format: "cjs",
watch: !prod,
target: "es2015",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
plugins: [
sveltePlugin({
preprocess: sveltePreprocess(),
compilerOptions: { css: true },
}),
],
outfile: "main.js",
})
.catch(() => process.exit(1));

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "0.5.0",
"version": "0.7.1",
"minAppVersion": "0.9.12",
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"author": "vorotamoroz",

939
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,12 @@
{
"name": "obsidian-livesync",
"version": "0.5.0",
"version": "0.7.1",
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"main": "main.js",
"type": "module",
"scripts": {
"dev": "rollup --config rollup.config.js -w",
"build": "rollup --config rollup.config.js --environment BUILD:production",
"dev": "node esbuild.config.mjs",
"build": "node esbuild.config.mjs production",
"lint": "eslint src"
},
"keywords": [],
@@ -16,6 +17,7 @@
"@rollup/plugin-node-resolve": "^11.2.1",
"@rollup/plugin-typescript": "^8.2.1",
"@types/diff-match-patch": "^1.0.32",
"@types/pouchdb": "^6.4.0",
"@types/pouchdb-browser": "^6.1.3",
"@typescript-eslint/eslint-plugin": "^5.7.0",
"@typescript-eslint/parser": "^5.0.0",
@@ -25,7 +27,11 @@
"obsidian": "^0.13.11",
"rollup": "^2.32.1",
"tslib": "^2.2.0",
"typescript": "^4.2.4"
"typescript": "^4.2.4",
"builtin-modules": "^3.2.0",
"esbuild": "0.13.12",
"esbuild-svelte": "^0.6.0",
"svelte-preprocess": "^4.10.2"
},
"dependencies": {
"diff-match-patch": "^1.0.5",

132
src/DocumentHistoryModal.ts Normal file
View File

@@ -0,0 +1,132 @@
import { TFile, Modal, App } from "obsidian";
import { path2id, escapeStringToHTML } from "./utils";
import ObsidianLiveSyncPlugin from "./main";
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
export class DocumentHistoryModal extends Modal {
plugin: ObsidianLiveSyncPlugin;
range: HTMLInputElement;
contentView: HTMLDivElement;
info: HTMLDivElement;
fileInfo: HTMLDivElement;
showDiff = false;
file: string;
revs_info: PouchDB.Core.RevisionInfo[] = [];
constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile) {
super(app);
this.plugin = plugin;
this.file = file.path;
if (localStorage.getItem("ols-history-highlightdiff") == "1") {
this.showDiff = true;
}
}
async loadFile() {
const db = this.plugin.localDatabase;
const w = await db.localDatabase.get(path2id(this.file), { revs_info: true });
this.revs_info = w._revs_info.filter((e) => e.status == "available");
this.range.max = `${this.revs_info.length - 1}`;
this.range.value = this.range.max;
this.fileInfo.setText(`${this.file} / ${this.revs_info.length} revisions`);
await this.loadRevs();
}
async loadRevs() {
const db = this.plugin.localDatabase;
const index = this.revs_info.length - 1 - (this.range.value as any) / 1;
const rev = this.revs_info[index];
const w = await db.getDBEntry(path2id(this.file), { rev: rev.rev }, false, false);
if (w === false) {
this.info.innerHTML = "";
this.contentView.innerHTML = `Could not read this revision<br>(${rev.rev})`;
} else {
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
let result = "";
if (this.showDiff) {
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) {
const oldRev = this.revs_info[prevRevIdx].rev;
const w2 = await db.getDBEntry(path2id(this.file), { rev: oldRev }, false, false);
if (w2 != false) {
const dmp = new diff_match_patch();
const diff = dmp.diff_main(w2.data, w.data);
dmp.diff_cleanupSemantic(diff);
for (const v of diff) {
const x1 = v[0];
const x2 = v[1];
if (x1 == DIFF_DELETE) {
result += "<span class='history-deleted'>" + escapeStringToHTML(x2) + "</span>";
} else if (x1 == DIFF_EQUAL) {
result += "<span class='history-normal'>" + escapeStringToHTML(x2) + "</span>";
} else if (x1 == DIFF_INSERT) {
result += "<span class='history-added'>" + escapeStringToHTML(x2) + "</span>";
}
}
result = result.replace(/\n/g, "<br>");
} else {
result = escapeStringToHTML(w.data);
}
} else {
result = escapeStringToHTML(w.data);
}
} else {
result = escapeStringToHTML(w.data);
}
this.contentView.innerHTML = result;
}
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h2", { text: "Document History" });
this.fileInfo = contentEl.createDiv("");
this.fileInfo.addClass("op-info");
const divView = contentEl.createDiv("");
divView.addClass("op-flex");
divView.createEl("input", { type: "range" }, (e) => {
this.range = e;
e.addEventListener("change", (e) => {
this.loadRevs();
});
e.addEventListener("input", (e) => {
this.loadRevs();
});
});
contentEl
.createDiv("", (e) => {
e.createEl("label", {}, (label) => {
label.appendChild(
createEl("input", { type: "checkbox" }, (checkbox) => {
if (this.showDiff) {
checkbox.checked = true;
}
checkbox.addEventListener("input", (evt: any) => {
this.showDiff = checkbox.checked;
localStorage.setItem("ols-history-highlightdiff", this.showDiff == true ? "1" : "");
this.loadRevs();
});
})
);
label.appendText("Highlight diff");
});
})
.addClass("op-info");
this.info = contentEl.createDiv("");
this.info.addClass("op-info");
this.loadFile();
const div = contentEl.createDiv({ text: "Loading old revisions..." });
this.contentView = div;
div.addClass("op-scrollable");
div.addClass("op-pre");
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

View File

@@ -23,7 +23,7 @@ import {
MILSTONE_DOCID,
DatabaseConnectingStatus,
} from "./types";
import { resolveWithIgnoreKnownError, delay, path2id, runWithLock } from "./utils";
import { resolveWithIgnoreKnownError, delay, path2id, runWithLock, isPlainText } from "./utils";
import { Logger } from "./logger";
import { checkRemoteVersion, connectRemoteCouchDB, getLastPostFailedBySize } from "./utils_couchdb";
import { decrypt, encrypt } from "./e2ee";
@@ -121,7 +121,7 @@ export class LocalPouchDB {
this.changeHandler = this.cancelHandler(this.changeHandler);
this.localDatabase = null;
this.localDatabase = new PouchDB<EntryDoc>(this.dbname + "-livesync", {
auto_compaction: true,
auto_compaction: this.settings.useHistory ? false : true,
revs_limit: 100,
deterministic_revs: true,
});
@@ -501,18 +501,6 @@ export class LocalPouchDB {
Logger(`deleteDBEntryPrefix:deleted ${deleteCount} items, skipped ${notfound}`);
return true;
}
isPlainText(filename: string): boolean {
if (filename.endsWith(".md")) return true;
if (filename.endsWith(".txt")) return true;
if (filename.endsWith(".svg")) return true;
if (filename.endsWith(".html")) return true;
if (filename.endsWith(".csv")) return true;
if (filename.endsWith(".css")) return true;
if (filename.endsWith(".js")) return true;
if (filename.endsWith(".xml")) return true;
return false;
}
async putDBEntry(note: LoadedEntry) {
await this.waitForGCComplete();
let leftData = note.data;
@@ -524,7 +512,7 @@ export class LocalPouchDB {
let plainSplit = false;
let cacheUsed = 0;
const userpasswordHash = this.h32Raw(new TextEncoder().encode(this.settings.passphrase));
if (this.isPlainText(note._id)) {
if (isPlainText(note._id)) {
pieceSize = MAX_DOC_SIZE;
plainSplit = true;
}
@@ -1204,7 +1192,13 @@ export class LocalPouchDB {
}
return false;
}
async garbageCollect() {
// if (this.settings.useHistory) {
// Logger("GC skipped for using history", LOG_LEVEL.VERBOSE);
// return;
// }
// NOTE:Garbage collection could break old revisions.
await runWithLock("replicate", true, async () => {
if (this.gcRunning) return;
this.gcRunning = true;
@@ -1218,29 +1212,36 @@ export class LocalPouchDB {
let usedPieces: string[] = [];
Logger("Collecting Garbage");
do {
const result = await this.localDatabase.allDocs({ include_docs: true, skip: c, limit: 500, conflicts: true });
const result = await this.localDatabase.allDocs({ include_docs: false, skip: c, limit: 2000, conflicts: true });
readCount = result.rows.length;
Logger("checked:" + readCount);
if (readCount > 0) {
//there are some result
for (const v of result.rows) {
const doc = v.doc;
if (doc.type == "newnote" || doc.type == "plain") {
// used pieces memo.
usedPieces = Array.from(new Set([...usedPieces, ...doc.children]));
if (doc._conflicts) {
for (const cid of doc._conflicts) {
const p = await this.localDatabase.get<EntryDoc>(doc._id, { rev: cid });
if (p.type == "newnote" || p.type == "plain") {
usedPieces = Array.from(new Set([...usedPieces, ...p.children]));
if (v.id.startsWith("h:")) {
hashPieces = Array.from(new Set([...hashPieces, v.id]));
} else {
const docT = await this.localDatabase.get(v.id, { revs_info: true });
const revs = docT._revs_info;
// console.log(`revs:${revs.length}`)
for (const rev of revs) {
if (rev.status != "available") continue;
// console.log(`id:${docT._id},rev:${rev.rev}`);
const doc = await this.localDatabase.get(v.id, { rev: rev.rev });
if ("children" in doc) {
// used pieces memo.
usedPieces = Array.from(new Set([...usedPieces, ...doc.children]));
if (doc._conflicts) {
for (const cid of doc._conflicts) {
const p = await this.localDatabase.get<EntryDoc>(doc._id, { rev: cid });
if (p.type == "newnote" || p.type == "plain") {
usedPieces = Array.from(new Set([...usedPieces, ...p.children]));
}
}
}
}
}
}
if (doc.type == "leaf") {
// all pieces.
hashPieces = Array.from(new Set([...hashPieces, doc._id]));
}
}
}
c += readCount;

View File

@@ -1,6 +1,6 @@
import { App, Notice, PluginSettingTab, Setting, sanitizeHTMLToDom } from "obsidian";
import { EntryDoc, LOG_LEVEL } from "./types";
import { escapeStringToHTML, versionNumberString2Number, path2id, id2path, runWithLock } from "./utils";
import { path2id, id2path, runWithLock } from "./utils";
import { Logger } from "./logger";
import { connectRemoteCouchDB } from "./utils_couchdb";
import { testCrypt } from "./e2ee";
@@ -602,6 +602,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
})
);
new Setting(containerMiscellaneousEl)
.setName("Use history (beta)")
.setDesc("Use history dialog (Restart required, auto compaction would be disabled, and more storage will be consumed)")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.useHistory).onChange(async (value) => {
this.plugin.settings.useHistory = value;
await this.plugin.saveSettings();
})
);
addScreenElement("40", containerMiscellaneousEl);
const containerHatchEl = containerEl.createDiv();
@@ -821,14 +830,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
toggle.setValue(this.plugin.settings.usePluginSync).onChange(async (value) => {
this.plugin.settings.usePluginSync = value;
await this.plugin.saveSettings();
updatePluginPane();
})
);
new Setting(containerPluginSettings).setName("Show own plugins and settings").addToggle((toggle) =>
toggle.setValue(this.plugin.settings.showOwnPlugins).onChange(async (value) => {
this.plugin.settings.showOwnPlugins = value;
await this.plugin.saveSettings();
updatePluginPane();
})
);
@@ -868,240 +875,26 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.setDesc("")
.addText((text) => {
text.setPlaceholder("desktop-main")
.setValue(this.plugin.settings.deviceAndVaultName)
.setValue(this.plugin.deviceAndVaultName)
.onChange(async (value) => {
this.plugin.settings.deviceAndVaultName = value;
this.plugin.deviceAndVaultName = value;
await this.plugin.saveSettings();
});
// text.inputEl.setAttribute("type", "password");
});
new Setting(containerPluginSettings)
.setName("Open")
.setDesc("Open the plugin dialog")
.addButton((button) => {
button
.setButtonText("Open")
.setDisabled(false)
.onClick(() => {
this.plugin.showPluginSyncModal();
});
});
updateDisabledOfDeviceAndVaultName();
const sweepPlugin = async (showMessage: boolean) => {
if (!this.plugin.settings.usePluginSync) {
return;
}
await this.plugin.sweepPlugin(showMessage);
updatePluginPane();
};
const updatePluginPane = async () => {
pluginConfig.innerHTML = "<div class='sls-plugins-wrap'>Retrieving...</div>";
const { plugins, allPlugins, thisDevicePlugins } = await this.plugin.getPluginList();
let html = `
<div class='sls-plugins-wrap'>
<table class='sls-plugins-tbl'>
`;
for (const vaults in plugins) {
if (!this.plugin.settings.showOwnPlugins && vaults == this.plugin.settings.deviceAndVaultName) continue;
html += `
<tr>
<th colspan=1 class='sls-plugins-tbl-device-head'>${escapeStringToHTML(vaults)}</th>
<td class='sls-plugins-tbl-device-head sls-plugins-tbl-buttons'>
<button class='sls-plugin-apply-all-newer-plugin mod-cta' data-key="${vaults}" aria-label="Apply all newer (without setting)">⚡</button>
<button class='sls-plugin-apply-all-newer-setting mod-cta' data-key="${vaults}" aria-label="Apply all newer settings">📚</button>
<button class='sls-plugin-delete mod-warning' data-key="${vaults}" aria-label="Delete">❌</button>
</td>
</tr>`;
for (const v of plugins[vaults]) {
const mtime = v.mtime == 0 ? "-" : new Date(v.mtime).toLocaleString();
let settingApplyable: boolean | string = "-";
let settingFleshness = "";
let isSameVersion = false;
let isSameContents = false;
if (thisDevicePlugins[v.manifest.id]) {
if (thisDevicePlugins[v.manifest.id].manifest.version == v.manifest.version) {
isSameVersion = true;
}
if (thisDevicePlugins[v.manifest.id].styleCss == v.styleCss && thisDevicePlugins[v.manifest.id].mainJs == v.mainJs && thisDevicePlugins[v.manifest.id].manifestJson == v.manifestJson) {
isSameContents = true;
}
}
if (thisDevicePlugins[v.manifest.id] && v.dataJson) {
// have this plugin.
const localSetting = thisDevicePlugins[v.manifest.id].dataJson || null;
try {
const remoteSetting = v.dataJson;
if (!localSetting) {
settingFleshness = "newer";
settingApplyable = true;
} else if (localSetting == remoteSetting) {
settingApplyable = "even";
} else {
if (v.mtime > thisDevicePlugins[v.manifest.id].mtime) {
settingFleshness = "newer";
} else {
settingFleshness = "older";
}
settingApplyable = true;
}
} catch (ex) {
settingApplyable = "could not decrypt";
}
} else if (!v.dataJson) {
settingApplyable = "N/A";
}
// very ugly way.
const piece = `
<tr class='divider'>
<th colspan=2></th>
</tr>
<tr>
<th class='sls-table-head'>${escapeStringToHTML(v.manifest.name)}</th>
<td class="sls-table-tail tcenter">${isSameContents ? "even" : `<button data-key='${v._id}' class='apply-plugin-version mod-cta'>Use (${isSameVersion ? "=" : ""}${v.manifest.version}) </button>`}</td>
</tr>
<tr>
<td class="sls-table-head tcenter">${escapeStringToHTML(mtime)}</td>
<td class="sls-table-tail tcenter">${settingApplyable === true ? "<button data-key='" + v._id + "' class='apply-plugin-data mod-cta'>Apply (" + settingFleshness + ")</button>" : settingApplyable}</td>
</tr>
`;
html += piece;
}
html += `
<tr class='divider'>
<th colspan=2></th>
</tr>
`;
}
html += "</table></div>";
pluginConfig.innerHTML = html;
pluginConfig.querySelectorAll(".apply-plugin-data").forEach((e) =>
e.addEventListener("click", async (evt) => {
const plugin = allPlugins[e.attributes.getNamedItem("data-key").value];
Logger(`Updating plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE);
await this.plugin.applyPluginData(plugin);
Logger(`Setting done:${plugin.manifest.name}`, LOG_LEVEL.NOTICE);
await sweepPlugin(true);
})
);
pluginConfig.querySelectorAll(".apply-plugin-version").forEach((e) =>
e.addEventListener("click", async (evt) => {
const plugin = allPlugins[e.attributes.getNamedItem("data-key").value];
Logger(`Setting plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE);
await this.plugin.applyPlugin(plugin);
Logger(`Updated plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE);
await sweepPlugin(true);
})
);
pluginConfig.querySelectorAll(".sls-plugin-apply-all-newer-plugin").forEach((e) =>
e.addEventListener("click", async (evt) => {
Logger("Apply all newer plugins.", LOG_LEVEL.NOTICE);
const vaultname = e.attributes.getNamedItem("data-key").value;
const plugins = Object.values(allPlugins).filter((e) => e.deviceVaultName == vaultname && e.manifest.id != "obsidian-livesync");
for (const plugin of plugins) {
const currentPlugin = thisDevicePlugins[plugin.manifest.id];
if (currentPlugin) {
const thisVersion = versionNumberString2Number(plugin.manifest.version);
const currentVersion = versionNumberString2Number(currentPlugin.manifest.version);
if (thisVersion > currentVersion) {
Logger(`Updating plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE);
await this.plugin.applyPlugin(plugin);
Logger(`Updated plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE);
} else {
Logger(`Plugin ${plugin.manifest.name} is not new`);
}
} else {
Logger(`Updating plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE);
await this.plugin.applyPlugin(plugin);
Logger(`Updated plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE);
}
}
await sweepPlugin(true);
Logger("Done", LOG_LEVEL.NOTICE);
})
);
pluginConfig.querySelectorAll(".sls-plugin-apply-all-newer-setting").forEach((e) =>
e.addEventListener("click", async (evt) => {
Logger("Apply all newer settings.", LOG_LEVEL.NOTICE);
const vaultname = e.attributes.getNamedItem("data-key").value;
const plugins = Object.values(allPlugins).filter((e) => e.deviceVaultName == vaultname && e.manifest.id != "obsidian-livesync");
for (const plugin of plugins) {
const currentPlugin = thisDevicePlugins[plugin.manifest.id];
if (currentPlugin) {
const thisVersion = plugin.mtime;
const currentVersion = currentPlugin.mtime;
if (thisVersion > currentVersion) {
Logger(`Setting plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE);
await this.plugin.applyPluginData(plugin);
Logger(`Setting done:${plugin.manifest.name}`, LOG_LEVEL.NOTICE);
} else {
Logger(`Setting ${plugin.manifest.name} is not new`);
}
} else {
Logger(`Setting plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE);
await this.plugin.applyPluginData(plugin);
Logger(`Setting done:${plugin.manifest.name}`, LOG_LEVEL.NOTICE);
}
}
await sweepPlugin(true);
Logger("Done", LOG_LEVEL.NOTICE);
})
);
pluginConfig.querySelectorAll(".sls-plugin-delete").forEach((e) =>
e.addEventListener("click", async (evt) => {
const db = this.plugin.localDatabase.localDatabase;
const vaultname = e.attributes.getNamedItem("data-key").value;
const oldDocs = await db.allDocs({ startkey: `ps:${vaultname}-`, endkey: `ps:${vaultname}.`, include_docs: true });
Logger(`Deleting ${vaultname}`, LOG_LEVEL.NOTICE);
const delDocs = oldDocs.rows.map((e) => {
e.doc._deleted = true;
return e.doc;
});
await db.bulkDocs(delDocs);
Logger(`Deleted ${vaultname}`, LOG_LEVEL.NOTICE);
await this.plugin.replicate(true);
await updatePluginPane();
})
);
};
const pluginConfig = containerPluginSettings.createEl("div");
new Setting(containerPluginSettings)
.setName("Reload")
.setDesc("Replicate once and reload the list")
.addButton((button) =>
button
.setButtonText("Reload")
.setDisabled(false)
.onClick(async () => {
if (!this.plugin.settings.usePluginSync) {
return;
}
await this.plugin.replicate(true);
await updatePluginPane();
})
);
new Setting(containerPluginSettings)
.setName("Save plugins into the database")
.setDesc("")
.addButton((button) =>
button
.setButtonText("Save plugins")
.setDisabled(false)
.onClick(async () => {
if (!this.plugin.settings.usePluginSync) {
return;
}
Logger("Save plugins.", LOG_LEVEL.NOTICE);
await sweepPlugin(true);
Logger("All plugins have been saved.", LOG_LEVEL.NOTICE);
await this.plugin.replicate(true);
})
);
new Setting(containerPluginSettings)
.setName("Check updates")
.setDesc("")
.addButton((button) =>
button
.setButtonText("Check")
.setDisabled(false)
.onClick(async () => {
Logger("Checking plugins.", LOG_LEVEL.NOTICE);
await this.plugin.checkPluginUpdate();
})
);
updatePluginPane();
addScreenElement("60", containerPluginSettings);

290
src/PluginPane.svelte Normal file
View File

@@ -0,0 +1,290 @@
<script lang="ts">
import ObsidianLiveSyncPlugin from "./main";
import { onMount } from "svelte";
import { DevicePluginList, PluginDataEntry } from "./types";
import { versionNumberString2Number } from "./utils";
type JudgeResult = "" | "NEWER" | "EVEN" | "EVEN_BUT_DIFFERENT" | "OLDER" | "REMOTE_ONLY";
interface PluginDataEntryDisp extends PluginDataEntry {
versionInfo: string;
mtimeInfo: string;
mtimeFlag: JudgeResult;
versionFlag: JudgeResult;
}
export let plugin: ObsidianLiveSyncPlugin;
let plugins: PluginDataEntry[] = [];
let deviceAndPlugins: { [key: string]: PluginDataEntryDisp[] } = {};
let devicePluginList: [string, PluginDataEntryDisp[]][] = [];
let ownPlugins: DevicePluginList = null;
let showOwnPlugins = false;
let targetList: { [key: string]: boolean } = {};
function saveTargetList() {
window.localStorage.setItem("ols-plugin-targetlist", JSON.stringify(targetList));
}
function loadTargetList() {
let e = window.localStorage.getItem("ols-plugin-targetlist") || "{}";
try {
targetList = JSON.parse(e);
} catch (_) {
// NO OP.
}
}
function clearSelection() {
targetList = {};
}
async function updateList() {
let x = await plugin.getPluginList();
ownPlugins = x.thisDevicePlugins;
plugins = Object.values(x.allPlugins);
let targetListItems = Array.from(new Set(plugins.map((e) => e.deviceVaultName + "---" + e.manifest.id)));
let newTargetList: { [key: string]: boolean } = {};
for (const id of targetListItems) {
for (const tag of ["---plugin", "---setting"]) {
newTargetList[id + tag] = id + tag in targetList && targetList[id + tag];
}
}
targetList = newTargetList;
saveTargetList();
}
$: {
deviceAndPlugins = {};
for (const p of plugins) {
if (p.deviceVaultName == plugin.deviceAndVaultName && !showOwnPlugins) {
continue;
}
if (!(p.deviceVaultName in deviceAndPlugins)) {
deviceAndPlugins[p.deviceVaultName] = [];
}
let dispInfo: PluginDataEntryDisp = { ...p, versionInfo: "", mtimeInfo: "", versionFlag: "", mtimeFlag: "" };
dispInfo.versionInfo = p.manifest.version;
let x = new Date().getTime() / 1000;
let mtime = p.mtime / 1000;
let diff = (x - mtime) / 60;
if (p.mtime == 0) {
dispInfo.mtimeInfo = `-`;
} else if (diff < 60) {
dispInfo.mtimeInfo = `${diff | 0} Mins ago`;
} else if (diff < 60 * 24) {
dispInfo.mtimeInfo = `${(diff / 60) | 0} Hours ago`;
} else if (diff < 60 * 24 * 10) {
dispInfo.mtimeInfo = `${(diff / (60 * 24)) | 0} Days ago`;
} else {
dispInfo.mtimeInfo = new Date(dispInfo.mtime).toLocaleString();
}
// compare with own plugin
let id = p.manifest.id;
if (id in ownPlugins) {
// Which we have.
const ownPlugin = ownPlugins[id];
let localVer = versionNumberString2Number(ownPlugin.manifest.version);
let pluginVer = versionNumberString2Number(p.manifest.version);
if (localVer > pluginVer) {
dispInfo.versionFlag = "OLDER";
} else if (localVer == pluginVer) {
if (ownPlugin.manifestJson + (ownPlugin.styleCss ?? "") + ownPlugin.mainJs != p.manifestJson + (p.styleCss ?? "") + p.mainJs) {
dispInfo.versionFlag = "EVEN_BUT_DIFFERENT";
} else {
dispInfo.versionFlag = "EVEN";
}
} else if (localVer < pluginVer) {
dispInfo.versionFlag = "NEWER";
}
if ((ownPlugin.dataJson ?? "") == (p.dataJson ?? "")) {
if (ownPlugin.mtime == 0 && p.mtime == 0) {
dispInfo.mtimeFlag = "";
} else {
dispInfo.mtimeFlag = "EVEN";
}
} else {
if (((ownPlugin.mtime / 1000) | 0) > ((p.mtime / 1000) | 0)) {
dispInfo.mtimeFlag = "OLDER";
} else if (((ownPlugin.mtime / 1000) | 0) == ((p.mtime / 1000) | 0)) {
dispInfo.mtimeFlag = "EVEN_BUT_DIFFERENT";
} else if (((ownPlugin.mtime / 1000) | 0) < ((p.mtime / 1000) | 0)) {
dispInfo.mtimeFlag = "NEWER";
}
}
} else {
dispInfo.versionFlag = "REMOTE_ONLY";
dispInfo.mtimeFlag = "REMOTE_ONLY";
}
deviceAndPlugins[p.deviceVaultName].push(dispInfo);
}
devicePluginList = Object.entries(deviceAndPlugins);
}
function getDispString(stat: JudgeResult): string {
if (stat == "") return "";
if (stat == "NEWER") return " (Newer)";
if (stat == "OLDER") return " (Older)";
if (stat == "EVEN") return " (Even)";
if (stat == "EVEN_BUT_DIFFERENT") return " (Even but different)";
if (stat == "REMOTE_ONLY") return " (Remote Only)";
return "";
}
onMount(async () => {
loadTargetList();
await updateList();
});
function toggleShowOwnPlugins() {
showOwnPlugins = !showOwnPlugins;
}
function toggleTarget(key: string) {
targetList[key] = !targetList[key];
saveTargetList();
}
function toggleAll(devicename: string) {
for (const c in targetList) {
if (c.startsWith(devicename)) {
targetList[c] = true;
}
}
}
async function sweepPlugins() {
//@ts-ignore
await plugin.app.plugins.loadManifests();
await plugin.sweepPlugin(true);
updateList();
}
async function applyPlugins() {
for (const c in targetList) {
if (targetList[c] == true) {
const [deviceAndVault, id, opt] = c.split("---");
if (deviceAndVault in deviceAndPlugins) {
const entry = deviceAndPlugins[deviceAndVault].find((e) => e.manifest.id == id);
if (entry) {
if (opt == "plugin") {
if (entry.versionFlag != "EVEN") await plugin.applyPlugin(entry);
} else if (opt == "setting") {
if (entry.mtimeFlag != "EVEN") await plugin.applyPluginData(entry);
}
}
}
}
}
//@ts-ignore
await plugin.app.plugins.loadManifests();
await plugin.sweepPlugin(true);
updateList();
}
async function checkUpdates() {
await plugin.checkPluginUpdate();
}
async function replicateAndRefresh() {
await plugin.replicate(true);
updateList();
}
</script>
<div>
<h1>Plugins and their settings</h1>
<div class="ols-plugins-div-buttons">
Show own items
<div class="checkbox-container" class:is-enabled={showOwnPlugins} on:click={toggleShowOwnPlugins} />
</div>
<div class="sls-plugins-wrap">
<table class="sls-plugins-tbl">
<tr style="position:sticky">
<th class="sls-plugins-tbl-device-head">Name</th>
<th class="sls-plugins-tbl-device-head">Info</th>
<th class="sls-plugins-tbl-device-head">Target</th>
</tr>
{#if devicePluginList.length == 0}
<tr>
<td colspan="3" class="sls-table-tail tcenter"> Retrieving... </td>
</tr>
{/if}
{#each devicePluginList as [deviceName, devicePlugins]}
<tr>
<th colspan="2" class="sls-plugins-tbl-device-head">{deviceName}</th>
<th class="sls-plugins-tbl-device-head">
<button class="mod-cta" on:click={() => toggleAll(deviceName)}>✔</button>
</th>
</tr>
{#each devicePlugins as plugin}
<tr>
<td class="sls-table-head">{plugin.manifest.name}</td>
<td class="sls-table-tail tcenter">{plugin.versionInfo}{getDispString(plugin.versionFlag)}</td>
<td class="sls-table-tail tcenter">
{#if plugin.versionFlag === "EVEN" || plugin.versionFlag === ""}
-
{:else}
<div class="wrapToggle">
<div
class="checkbox-container"
class:is-enabled={targetList[plugin.deviceVaultName + "---" + plugin.manifest.id + "---plugin"]}
on:click={() => toggleTarget(plugin.deviceVaultName + "---" + plugin.manifest.id + "---plugin")}
/>
</div>
{/if}
</td>
</tr>
<tr>
<td class="sls-table-head">Settings</td>
<td class="sls-table-tail tcenter">{plugin.mtimeInfo}{getDispString(plugin.mtimeFlag)}</td>
<td class="sls-table-tail tcenter">
{#if plugin.mtimeFlag === "EVEN" || plugin.mtimeFlag === ""}
-
{:else}
<div class="wrapToggle">
<div
class="checkbox-container"
class:is-enabled={targetList[plugin.deviceVaultName + "---" + plugin.manifest.id + "---setting"]}
on:click={() => toggleTarget(plugin.deviceVaultName + "---" + plugin.manifest.id + "---setting")}
/>
</div>
{/if}
</td>
</tr>
<tr class="divider">
<th colspan="3" />
</tr>
{/each}
{/each}
</table>
</div>
<div class="ols-plugins-div-buttons">
<button class="" on:click={replicateAndRefresh}>Replicate and refresh</button>
<button class="" on:click={clearSelection}>Clear Selection</button>
</div>
<div class="ols-plugins-div-buttons">
<button class="mod-cta" on:click={checkUpdates}>Check Updates</button>
<button class="mod-cta" on:click={sweepPlugins}>Sweep installed</button>
<button class="mod-cta" on:click={applyPlugins}>Apply all</button>
</div>
<!-- <div class="ols-plugins-div-buttons">-->
<!-- <button class="mod-warning" on:click={applyPlugins}>Delete all selected</button>-->
<!-- </div>-->
</div>
<style>
.ols-plugins-div-buttons {
display: flex;
flex-direction: row;
justify-content: flex-end;
margin-top: 8px;
}
.wrapToggle {
display: flex;
justify-content: center;
align-content: center;
}
</style>

View File

@@ -1,4 +1,4 @@
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest } from "obsidian";
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, Modal, App } from "obsidian";
import { diff_match_patch } from "diff-match-patch";
import {
@@ -18,12 +18,43 @@ import {
diff_result,
FLAGMD_REDFLAG,
} from "./types";
import { base64ToString, arrayBufferToBase64, base64ToArrayBuffer, isValidPath, versionNumberString2Number, id2path, path2id, runWithLock, shouldBeIgnored, getProcessingCounts, setLockNotifier } from "./utils";
import { base64ToString, arrayBufferToBase64, base64ToArrayBuffer, isValidPath, versionNumberString2Number, id2path, path2id, runWithLock, shouldBeIgnored, getProcessingCounts, setLockNotifier, isPlainText } from "./utils";
import { Logger, setLogger } from "./logger";
import { LocalPouchDB } from "./LocalPouchDB";
import { LogDisplayModal } from "./LogDisplayModal";
import { ConflictResolveModal } from "./ConflictResolveModal";
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
import { DocumentHistoryModal } from "./DocumentHistoryModal";
import PluginPane from "./PluginPane.svelte";
class PluginDialogModal extends Modal {
plugin: ObsidianLiveSyncPlugin;
logEl: HTMLDivElement;
component: PluginPane = null;
constructor(app: App, plugin: ObsidianLiveSyncPlugin) {
super(app);
this.plugin = plugin;
}
onOpen() {
const { contentEl } = this;
if (this.component == null) {
this.component = new PluginPane({
target: contentEl,
props: { plugin: this.plugin },
});
}
}
onClose() {
if (this.component != null) {
this.component.$destroy();
this.component = null;
}
}
}
export default class ObsidianLiveSyncPlugin extends Plugin {
settings: ObsidianLiveSyncSettings;
@@ -32,12 +63,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
statusBar: HTMLElement;
statusBar2: HTMLElement;
suspended: boolean;
deviceAndVaultName: string;
setInterval(handler: () => any, timeout?: number): number {
const timer = window.setInterval(handler, timeout);
this.registerInterval(timer);
return timer;
}
isRedFlagRaised(): boolean {
const redflag = this.app.vault.getAbstractFileByPath(normalizePath(FLAGMD_REDFLAG));
if (redflag != null) {
@@ -45,6 +78,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
return false;
}
showHistory(file: TFile) {
if (!this.settings.useHistory) {
Logger("You have to enable Use History in misc.", LOG_LEVEL.NOTICE);
} else {
new DocumentHistoryModal(this.app, this, file).open();
}
}
async onload() {
setLogger(this.addLog.bind(this)); // Logger moved to global.
Logger("loading plugin");
@@ -108,6 +150,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.periodicSync = this.periodicSync.bind(this);
this.setPeriodicSync = this.setPeriodicSync.bind(this);
this.getPluginList = this.getPluginList.bind(this);
// this.registerWatchEvents();
this.addSettingTab(new ObsidianLiveSyncSettingTab(this.app, this));
@@ -203,13 +246,47 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.saveSettings();
},
});
this.addCommand({
id: "livesync-history",
name: "Show history",
editorCallback: (editor: Editor, view: MarkdownView) => {
this.showHistory(view.file);
},
});
this.triggerRealizeSettingSyncMode = debounce(this.triggerRealizeSettingSyncMode.bind(this), 1000);
this.triggerCheckPluginUpdate = debounce(this.triggerCheckPluginUpdate.bind(this), 3000);
setLockNotifier(() => {
this.refreshStatusText();
});
this.addCommand({
id: "livesync-plugin-dialog",
name: "Show Plugins and their settings",
callback: () => {
this.showPluginSyncModal();
},
});
}
pluginDialog: PluginDialogModal = null;
showPluginSyncModal() {
if (this.pluginDialog != null) {
this.pluginDialog.open();
} else {
this.pluginDialog = new PluginDialogModal(this.app, this);
this.pluginDialog.open();
}
}
hidePluginSyncModal() {
if (this.pluginDialog != null) {
this.pluginDialog.close();
this.pluginDialog = null;
}
}
onunload() {
this.hidePluginSyncModal();
this.localDatabase.onunload();
if (this.gcTimerHandler != null) {
clearTimeout(this.gcTimerHandler);
@@ -235,6 +312,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
};
await this.localDatabase.initializeDatabase();
}
async garbageCollect() {
await this.localDatabase.garbageCollect();
}
@@ -243,19 +321,35 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
this.settings.workingEncrypt = this.settings.encrypt;
this.settings.workingPassphrase = this.settings.passphrase;
const lsname = "obsidian-live-sync-vaultanddevicename-" + this.app.vault.getName();
if (this.settings.deviceAndVaultName != "") {
if (!localStorage.getItem(lsname)) {
this.deviceAndVaultName = this.settings.deviceAndVaultName;
localStorage.setItem(lsname, this.deviceAndVaultName);
this.settings.deviceAndVaultName = "";
}
}
this.deviceAndVaultName = localStorage.getItem(lsname) || "";
}
triggerRealizeSettingSyncMode() {
(async () => await this.realizeSettingSyncMode())();
}
async saveSettings() {
const lsname = "obsidian-live-sync-vaultanddevicename-" + this.app.vault.getName();
localStorage.setItem(lsname, this.deviceAndVaultName || "");
await this.saveData(this.settings);
this.localDatabase.settings = this.settings;
this.triggerRealizeSettingSyncMode();
}
gcTimerHandler: any = null;
gcHook() {
if (this.settings.gcDelay == 0) return;
if (this.settings.useHistory) return;
const GC_DELAY = this.settings.gcDelay * 1000; // if leaving opening window, try GC,
if (this.gcTimerHandler != null) {
clearTimeout(this.gcTimerHandler);
@@ -266,6 +360,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.garbageCollect();
}, GC_DELAY);
}
registerWatchEvents() {
this.registerEvent(this.app.vault.on("modify", this.watchVaultChange));
this.registerEvent(this.app.vault.on("delete", this.watchVaultDelete));
@@ -278,6 +373,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
watchWindowVisiblity() {
this.watchWindowVisiblityAsync();
}
async watchWindowVisiblityAsync() {
if (this.settings.suspendFileWatching) return;
// if (this.suspended) return;
@@ -309,6 +405,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (this.settings.suspendFileWatching) return;
this.watchWorkspaceOpenAsync(file);
}
async watchWorkspaceOpenAsync(file: TFile) {
await this.applyBatchChange();
if (file == null) return;
@@ -319,10 +416,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
await this.showIfConflicted(file);
this.gcHook();
}
watchVaultCreate(file: TFile, ...args: any[]) {
if (this.settings.suspendFileWatching) return;
this.watchVaultChangeAsync(file, ...args);
}
watchVaultChange(file: TAbstractFile, ...args: any[]) {
if (!(file instanceof TFile)) {
return;
@@ -336,6 +435,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
this.watchVaultChangeAsync(file, ...args);
}
async applyBatchChange() {
if (!this.settings.batchSave || this.batchFileChange.length == 0) {
return [];
@@ -359,19 +459,23 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
return await Promise.all(promises);
});
}
batchFileChange: string[] = [];
async watchVaultChangeAsync(file: TFile, ...args: any[]) {
if (file instanceof TFile) {
await this.updateIntoDB(file);
this.gcHook();
}
}
watchVaultDelete(file: TAbstractFile) {
// When save is delayed, it should be cancelled.
this.batchFileChange = this.batchFileChange.filter((e) => e == file.path);
if (this.settings.suspendFileWatching) return;
this.watchVaultDeleteAsync(file);
this.watchVaultDeleteAsync(file).then(() => {});
}
async watchVaultDeleteAsync(file: TAbstractFile) {
if (file instanceof TFile) {
await this.deleteFromDB(file);
@@ -380,6 +484,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
this.gcHook();
}
GetAllFilesRecursively(file: TAbstractFile): TFile[] {
if (file instanceof TFile) {
return [file];
@@ -394,10 +499,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
throw new Error(`Filetype error:${file.path}`);
}
}
watchVaultRename(file: TAbstractFile, oldFile: any) {
if (this.settings.suspendFileWatching) return;
this.watchVaultRenameAsync(file, oldFile);
this.watchVaultRenameAsync(file, oldFile).then(() => {});
}
getFilePath(file: TAbstractFile): string {
if (file instanceof TFolder) {
if (file.isRoot()) return "";
@@ -409,6 +516,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
return this.getFilePath(file.parent) + "/" + file.name;
}
async watchVaultRenameAsync(file: TAbstractFile, oldFile: any) {
Logger(`${oldFile} renamed to ${file.path}`, LOG_LEVEL.VERBOSE);
try {
@@ -445,9 +553,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
this.gcHook();
}
addLogHook: () => void = null;
//--> Basic document Functions
notifies: { [key: string]: { notice: Notice; timer: NodeJS.Timeout; count: number } } = {};
// eslint-disable-next-line require-await
async addLog(message: any, level: LOG_LEVEL = LOG_LEVEL.INFO) {
if (level < LOG_LEVEL.INFO && this.settings && this.settings.lessInformationInLog) {
@@ -531,7 +641,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
await this.ensureDirectory(path);
try {
const newfile = await this.app.vault.createBinary(normalizePath(path), bin, { ctime: doc.ctime, mtime: doc.mtime });
const newfile = await this.app.vault.createBinary(normalizePath(path), bin, {
ctime: doc.ctime,
mtime: doc.mtime,
});
Logger("live : write to local (newfile:b) " + path);
this.app.vault.trigger("create", newfile);
} catch (ex) {
@@ -546,7 +659,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
await this.ensureDirectory(path);
try {
const newfile = await this.app.vault.create(normalizePath(path), doc.data, { ctime: doc.ctime, mtime: doc.mtime });
const newfile = await this.app.vault.create(normalizePath(path), doc.data, {
ctime: doc.ctime,
mtime: doc.mtime,
});
Logger("live : write to local (newfile:p) " + path);
this.app.vault.trigger("create", newfile);
} catch (ex) {
@@ -574,6 +690,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
}
}
async doc2storate_modify(docEntry: EntryBody, file: TFile, force?: boolean) {
const pathSrc = id2path(docEntry._id);
if (shouldBeIgnored(pathSrc)) {
@@ -581,7 +698,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
if (docEntry._deleted) {
//basically pass.
//but if there're no docs left, delete file.
//but if there are no docs left, delete file.
const lastDocs = await this.localDatabase.getDBEntry(pathSrc);
if (lastDocs === false) {
await this.deleteVaultItem(file);
@@ -641,6 +758,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
//eq.case
}
}
async handleDBChanged(change: EntryBody) {
const targetFile = this.app.vault.getAbstractFileByPath(id2path(change._id));
if (targetFile == null) {
@@ -660,6 +778,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
periodicSyncHandler: number = null;
//---> Sync
async parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<void> {
this.refreshStatusText();
@@ -686,61 +805,81 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.gcHook();
}
}
triggerCheckPluginUpdate() {
(async () => await this.checkPluginUpdate())();
}
async checkPluginUpdate() {
if (!this.settings.usePluginSync) return;
await this.sweepPlugin(false);
const { allPlugins, thisDevicePlugins } = await this.getPluginList();
const arrPlugins = Object.values(allPlugins);
let updateFound = false;
for (const plugin of arrPlugins) {
const currentPlugin = thisDevicePlugins[plugin.manifest.id];
if (currentPlugin) {
const thisVersion = versionNumberString2Number(plugin.manifest.version);
const currentVersion = versionNumberString2Number(currentPlugin.manifest.version);
if (thisVersion > currentVersion) {
Logger(`the device ${plugin.deviceVaultName} has the newer plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE);
const ownPlugin = thisDevicePlugins[plugin.manifest.id];
if (ownPlugin) {
const remoteVersion = versionNumberString2Number(plugin.manifest.version);
const ownVersion = versionNumberString2Number(ownPlugin.manifest.version);
if (remoteVersion > ownVersion) {
updateFound = true;
}
if (plugin.mtime > currentPlugin.mtime) {
Logger(`the device ${plugin.deviceVaultName} has the newer settings of the plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE);
if (((plugin.mtime / 1000) | 0) > ((ownPlugin.mtime / 1000) | 0) && (plugin.dataJson ?? "") != (ownPlugin.dataJson ?? "")) {
updateFound = true;
}
} else {
Logger(`the device ${plugin.deviceVaultName} has the new plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE);
}
}
if (updateFound) {
const fragment = createFragment((doc) => {
doc.createEl("a", null, (a) => {
a.text = "There're some new plugins or their settings";
a.addEventListener("click", () => this.showPluginSyncModal());
});
});
new Notice(fragment, 10000);
} else {
Logger("Everything is up to date.", LOG_LEVEL.NOTICE);
}
}
clearPeriodicSync() {
if (this.periodicSyncHandler != null) {
clearInterval(this.periodicSyncHandler);
this.periodicSyncHandler = null;
}
}
setPeriodicSync() {
if (this.settings.periodicReplication && this.settings.periodicReplicationInterval > 0) {
this.clearPeriodicSync();
this.periodicSyncHandler = this.setInterval(async () => await this.periodicSync(), Math.max(this.settings.periodicReplicationInterval, 30) * 1000);
}
}
async periodicSync() {
await this.replicate();
}
periodicPluginSweepHandler: number = null;
clearPluginSweep() {
if (this.periodicPluginSweepHandler != null) {
clearInterval(this.periodicPluginSweepHandler);
this.periodicPluginSweepHandler = null;
}
}
setPluginSweep() {
if (this.settings.autoSweepPluginsPeriodic) {
this.clearPluginSweep();
this.periodicPluginSweepHandler = this.setInterval(async () => await this.periodicPluginSweep(), PERIODIC_PLUGIN_SWEEP * 1000);
}
}
async periodicPluginSweep() {
await this.sweepPlugin(false);
}
async realizeSettingSyncMode() {
this.localDatabase.closeReplication();
this.clearPeriodicSync();
@@ -758,7 +897,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.setPeriodicSync();
this.setPluginSweep();
}
lastMessage = "";
refreshStatusText() {
const sent = this.localDatabase.docSent;
const arrived = this.localDatabase.docArrived;
@@ -791,10 +932,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
waiting = waiting.replace(/(🛫){10}/g, "🚀");
}
const procs = getProcessingCounts();
const procsDisp = procs==0?"":`${procs}`;
const procsDisp = procs == 0 ? "" : `${procs}`;
const message = `Sync:${w}${sent}${arrived}${waiting}${procsDisp}`;
this.setStatusBarText(message);
}
setStatusBarText(message: string) {
if (this.lastMessage != message) {
this.statusBar.setText(message);
@@ -808,6 +950,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.lastMessage = message;
}
}
async replicate(showMessage?: boolean) {
if (this.settings.versionUpFlash != "") {
new Notice("Open settings and check message, please.");
@@ -824,21 +967,26 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
await this.openDatabase();
await this.syncAllFiles(showingNotice);
}
async replicateAllToServer(showingNotice?: boolean) {
if (this.settings.autoSweepPlugins) {
await this.sweepPlugin(showingNotice);
}
return await this.localDatabase.replicateAllToServer(this.settings, showingNotice);
}
async markRemoteLocked() {
return await this.localDatabase.markRemoteLocked(this.settings, true);
}
async markRemoteUnlocked() {
return await this.localDatabase.markRemoteLocked(this.settings, false);
}
async markRemoteResolved() {
return await this.localDatabase.markRemoteResolved(this.settings);
}
async syncAllFiles(showingNotice?: boolean) {
// synchronize all files between database and storage.
let notice: Notice = null;
@@ -880,6 +1028,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
Logger(ex);
}
});
// @ts-ignore
if (!Promise.allSettled) {
await Promise.all(
procs.map((p) =>
@@ -895,6 +1044,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
)
);
} else {
// @ts-ignore
await Promise.allSettled(procs);
}
};
@@ -916,6 +1066,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
Logger("Initialize done!", LOG_LEVEL.NOTICE);
}
}
async deleteFolderOnDB(folder: TFolder) {
Logger(`delete folder:${folder.path}`);
await this.localDatabase.deleteDBEntryPrefix(folder.path + "/");
@@ -1000,6 +1151,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
return false;
}
/**
* Getting file conflicted status.
* @param path the file location
@@ -1060,6 +1212,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
diff: diff,
};
}
showMergeDialog(file: TFile, conflictCheckResult: diff_result): Promise<boolean> {
return new Promise((res, rej) => {
Logger("open conflict dialog", LOG_LEVEL.VERBOSE);
@@ -1105,10 +1258,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}).open();
});
}
conflictedCheckFiles: string[] = [];
// queueing the conflicted file check
conflictedCheckTimer: number;
queueConflictedCheck(file: TFile) {
this.conflictedCheckFiles = this.conflictedCheckFiles.filter((e) => e != file.path);
this.conflictedCheckFiles.push(file.path);
@@ -1130,6 +1285,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
}, 1000);
}
async showIfConflicted(file: TFile) {
await runWithLock("conflicted", false, async () => {
const conflictCheckResult = await this.getConflictedStatus(file.path);
@@ -1149,6 +1305,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
await this.showMergeDialog(file, conflictCheckResult);
});
}
async pullFile(filename: string, fileList?: TFile[], force?: boolean, rev?: string, waitForReady = true) {
const targetFile = this.app.vault.getAbstractFileByPath(id2path(filename));
if (targetFile == null) {
@@ -1168,6 +1325,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
//when to opened file;
}
async syncFileBetweenDBandStorage(file: TFile, fileList?: TFile[]) {
const doc = await this.localDatabase.getDBEntryMeta(file.path);
if (doc === false) return;
@@ -1201,7 +1359,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
await this.localDatabase.waitForGCComplete();
let content = "";
let datatype: "plain" | "newnote" = "newnote";
if (file.extension != "md") {
if (!isPlainText(file.name)) {
const contentBin = await this.app.vault.readBinary(file);
content = await arrayBufferToBase64(contentBin);
datatype = "newnote";
@@ -1241,6 +1399,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
await this.replicate();
}
}
async deleteFromDB(file: TFile) {
const fullpath = file.path;
Logger(`deleteDB By path:${fullpath}`);
@@ -1249,6 +1408,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
await this.replicate();
}
}
async deleteFromDBbyPath(fullpath: string) {
await this.localDatabase.deleteDBEntry(fullpath);
if (this.settings.syncOnSave && !this.suspended) {
@@ -1259,12 +1419,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
async resetLocalDatabase() {
await this.localDatabase.resetDatabase();
}
async tryResetRemoteDatabase() {
await this.localDatabase.tryResetRemoteDatabase(this.settings);
}
async tryCreateRemoteDatabase() {
await this.localDatabase.tryCreateRemoteDatabase(this.settings);
}
async getPluginList(): Promise<{ plugins: PluginList; allPlugins: DevicePluginList; thisDevicePlugins: DevicePluginList }> {
const db = this.localDatabase.localDatabase;
const docList = await db.allDocs<PluginDataEntry>({ startkey: `ps:`, endkey: `ps;`, include_docs: false });
@@ -1278,12 +1441,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
plugins[v.deviceVaultName].push(v);
allPlugins[v._id] = v;
if (v.deviceVaultName == this.settings.deviceAndVaultName) {
if (v.deviceVaultName == this.deviceAndVaultName) {
thisDevicePlugins[v.manifest.id] = v;
}
}
return { plugins, allPlugins, thisDevicePlugins };
}
async sweepPlugin(showMessage = false) {
if (!this.settings.usePluginSync) return;
await runWithLock("sweepplugin", false, async () => {
@@ -1292,13 +1456,17 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
Logger("You have to encrypt the database to use plugin setting sync.", LOG_LEVEL.NOTICE);
return;
}
if (!this.settings.deviceAndVaultName) {
if (!this.deviceAndVaultName) {
Logger("You have to set your device and vault name.", LOG_LEVEL.NOTICE);
return;
}
Logger("Sweeping plugins", logLevel);
const db = this.localDatabase.localDatabase;
const oldDocs = await db.allDocs({ startkey: `ps:${this.settings.deviceAndVaultName}-`, endkey: `ps:${this.settings.deviceAndVaultName}.`, include_docs: true });
const oldDocs = await db.allDocs({
startkey: `ps:${this.deviceAndVaultName}-`,
endkey: `ps:${this.deviceAndVaultName}.`,
include_docs: true,
});
Logger("OLD DOCS.", LOG_LEVEL.VERBOSE);
// sweep current plugin.
// @ts-ignore
@@ -1321,9 +1489,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
mtime = (await adapter.stat(path + "/data.json")).mtime;
}
const p: PluginDataEntry = {
_id: `ps:${this.settings.deviceAndVaultName}-${m.id}`,
_id: `ps:${this.deviceAndVaultName}-${m.id}`,
dataJson: pluginData["data.json"],
deviceVaultName: this.settings.deviceAndVaultName,
deviceVaultName: this.deviceAndVaultName,
mainJs: pluginData["main.js"],
styleCss: pluginData["styles.css"],
manifest: m,
@@ -1367,12 +1535,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
Logger(`Sweep plugin done.`, logLevel);
});
}
async applyPluginData(plugin: PluginDataEntry) {
await runWithLock("plugin-" + plugin.manifest.id, false, async () => {
const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/";
const adapter = this.app.vault.adapter;
// @ts-ignore
const stat = this.app.plugins.enabledPlugins[plugin.manifest.id];
const stat = this.app.plugins.enabledPlugins.has(plugin.manifest.id) == true;
if (stat) {
// @ts-ignore
await this.app.plugins.unloadPlugin(plugin.manifest.id);
@@ -1380,7 +1549,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
if (plugin.dataJson) await adapter.write(pluginTargetFolderPath + "data.json", plugin.dataJson);
Logger("wrote:" + pluginTargetFolderPath + "data.json", LOG_LEVEL.NOTICE);
// @ts-ignore
if (stat) {
// @ts-ignore
await this.app.plugins.loadPlugin(plugin.manifest.id);
@@ -1388,10 +1556,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
});
}
async applyPlugin(plugin: PluginDataEntry) {
await runWithLock("plugin-" + plugin.manifest.id, false, async () => {
// @ts-ignore
const stat = this.app.plugins.enabledPlugins[plugin.manifest.id];
const stat = this.app.plugins.enabledPlugins.has(plugin.manifest.id) == true;
if (stat) {
// @ts-ignore
await this.app.plugins.unloadPlugin(plugin.manifest.id);
@@ -1406,7 +1575,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
await adapter.write(pluginTargetFolderPath + "main.js", plugin.mainJs);
await adapter.write(pluginTargetFolderPath + "manifest.json", plugin.manifestJson);
if (plugin.styleCss) await adapter.write(pluginTargetFolderPath + "styles.css", plugin.styleCss);
// if (plugin.dataJson) await adapter.write(pluginTargetFolderPath + "data.json", plugin.dataJson);
if (stat) {
// @ts-ignore
await this.app.plugins.loadPlugin(plugin.manifest.id);

View File

@@ -2,6 +2,7 @@
// and cloudant limitation is 1MB , we use 900kb;
import { PluginManifest } from "obsidian";
import * as PouchDB from "pouchdb";
export const MAX_DOC_SIZE = 1000; // for .md file, but if delimiters exists. use that before.
export const MAX_DOC_SIZE_BIN = 102400; // 100kb
@@ -58,6 +59,7 @@ export interface ObsidianLiveSyncSettings {
checkIntegrityOnSave: boolean;
batch_size: number;
batches_limit: number;
useHistory: boolean;
}
export const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
@@ -98,6 +100,7 @@ export const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
checkIntegrityOnSave: false,
batch_size: 250,
batches_limit: 40,
useHistory: false,
};
export const PERIODIC_PLUGIN_SWEEP = 60;

View File

@@ -234,3 +234,16 @@ export function runWithLock<T>(key: unknown, ignoreWhenRunning: boolean, proc: (
});
}
}
export function isPlainText(filename: string): boolean {
if (filename.endsWith(".md")) return true;
if (filename.endsWith(".txt")) return true;
if (filename.endsWith(".svg")) return true;
if (filename.endsWith(".html")) return true;
if (filename.endsWith(".csv")) return true;
if (filename.endsWith(".css")) return true;
if (filename.endsWith(".js")) return true;
if (filename.endsWith(".xml")) return true;
return false;
}

View File

@@ -34,11 +34,13 @@
.sls-plugins-wrap {
display: flex;
flex-grow: 1;
/* overflow: scroll; */
max-height: 50vh;
overflow-y: scroll;
}
.sls-plugins-tbl {
border: 1px solid var(--background-modifier-border);
width: 100%;
max-height: 80%;
}
.divider th {
border-top: 1px solid var(--background-modifier-border);
@@ -140,3 +142,33 @@ div.sls-setting-menu-btn {
background-color: var(--background-secondary-alt);
color: var(--text-accent);
}
.op-flex {
display: flex;
}
.op-flex input {
display: inline-flex;
flex-grow: 1;
margin-bottom: 8px;
}
.op-info {
display: inline-flex;
flex-grow: 1;
border-bottom: 1px solid var(--background-modifier-border);
width: 100%;
margin-bottom: 4px;
padding-bottom: 4px;
}
.history-added {
color: var(--text-on-accent);
background-color: var(--text-accent);
}
.history-normal {
color: var(--text-normal);
}
.history-deleted {
color: var(--text-on-accent);
background-color: var(--text-muted);
text-decoration: line-through;
}

View File

@@ -1,21 +1,17 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "es6",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"types": ["svelte", "node"],
// "importsNotUsedAsValues": "error",
"importHelpers": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"strictFunctionTypes": true,
"alwaysStrict": true,
"lib": ["dom", "es5", "ES6", "ES7", "es2020"]
"lib": ["es2018", "DOM", "ES5", "ES6", "ES7"]
},
"include": ["./src/*.ts"],
// "files": ["./src/main.ts"],
"include": ["**/*.ts"],
"exclude": ["pouchdb-browser-webpack"]
}