mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-02-27 06:28:47 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a8c76efb5 | ||
|
|
75ee63e573 | ||
|
|
3435efaf89 | ||
|
|
57f91eb407 | ||
|
|
50916aef0b | ||
|
|
8126bb6c02 |
382
main.ts
382
main.ts
@@ -1,4 +1,4 @@
|
||||
import { App, debounce, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView } from "obsidian";
|
||||
import { App, debounce, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest } from "obsidian";
|
||||
import { PouchDB } from "./pouchdb-browser-webpack/dist/pouchdb-browser";
|
||||
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
|
||||
import xxhash from "xxhash-wasm";
|
||||
@@ -51,6 +51,9 @@ interface ObsidianLiveSyncSettings {
|
||||
doNotDeleteFolder: boolean;
|
||||
resolveConflictsByNewerFile: boolean;
|
||||
batchSave: boolean;
|
||||
deviceAndVaultName: string;
|
||||
usePluginSettings: boolean;
|
||||
showOwnPlugins: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
|
||||
@@ -80,7 +83,11 @@ const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
|
||||
doNotDeleteFolder: false,
|
||||
resolveConflictsByNewerFile: false,
|
||||
batchSave: false,
|
||||
deviceAndVaultName: "",
|
||||
usePluginSettings: false,
|
||||
showOwnPlugins: false,
|
||||
};
|
||||
|
||||
interface Entry {
|
||||
_id: string;
|
||||
data: string;
|
||||
@@ -121,6 +128,22 @@ type LoadedEntry = Entry & {
|
||||
datatype: "plain" | "newnote";
|
||||
};
|
||||
|
||||
interface PluginDataEntry {
|
||||
_id: string;
|
||||
deviceVaultName: string;
|
||||
mtime: number;
|
||||
manifest: PluginManifest;
|
||||
mainJs: string;
|
||||
manifestJson: string;
|
||||
styleCss?: string;
|
||||
// it must be encrypted.
|
||||
dataJson?: string;
|
||||
_rev?: string;
|
||||
_deleted?: boolean;
|
||||
_conflicts?: string[];
|
||||
type: "plugin";
|
||||
}
|
||||
|
||||
interface EntryLeaf {
|
||||
_id: string;
|
||||
data: string;
|
||||
@@ -156,7 +179,7 @@ interface EntryNodeInfo {
|
||||
}
|
||||
|
||||
type EntryBody = Entry | NewEntry | PlainEntry;
|
||||
type EntryDoc = EntryBody | LoadedEntry | EntryLeaf | EntryVersionInfo | EntryMilestoneInfo | EntryNodeInfo;
|
||||
type EntryDoc = EntryBody | LoadedEntry | EntryLeaf | EntryVersionInfo | EntryMilestoneInfo | EntryNodeInfo | PluginDataEntry;
|
||||
|
||||
type diff_result_leaf = {
|
||||
rev: string;
|
||||
@@ -330,7 +353,7 @@ const bumpRemoteVersion = async (db: PouchDB.Database, barrier: number = VER): P
|
||||
};
|
||||
|
||||
function isValidPath(filename: string): boolean {
|
||||
let regex = /[\u0000-\u001f]|[\\"':?<>|*$]/g;
|
||||
let regex = /[\u0000-\u001f]|[\\"':?<>|*]/g;
|
||||
let x = filename.replace(regex, "_");
|
||||
let win = /(\\|\/)(COM\d|LPT\d|CON|PRN|AUX|NUL|CLOCK$)($|\.)/gi;
|
||||
let sx = (x = x.replace(win, "/_"));
|
||||
@@ -563,6 +586,7 @@ class LocalPouchDB {
|
||||
// this.initializeDatabase();
|
||||
}
|
||||
close() {
|
||||
Logger("Database closed (by close)");
|
||||
this.isReady = false;
|
||||
if (this.changeHandler != null) {
|
||||
this.changeHandler.cancel();
|
||||
@@ -624,6 +648,7 @@ class LocalPouchDB {
|
||||
await this.localDatabase.put(nodeinfo);
|
||||
}
|
||||
this.localDatabase.on("close", () => {
|
||||
Logger("Database closed.");
|
||||
this.isReady = false;
|
||||
});
|
||||
this.nodeid = nodeinfo.nodeid;
|
||||
@@ -642,6 +667,7 @@ class LocalPouchDB {
|
||||
});
|
||||
this.changeHandler = changes;
|
||||
this.isReady = true;
|
||||
Logger("Database is now ready.");
|
||||
}
|
||||
|
||||
async prepareHashFunctions() {
|
||||
@@ -667,7 +693,7 @@ class LocalPouchDB {
|
||||
waitForLeafReady(id: string): Promise<boolean> {
|
||||
return new Promise((res, rej) => {
|
||||
// Set timeout.
|
||||
let timer = setTimeout(() => rej(false), LEAF_WAIT_TIMEOUT);
|
||||
let timer = setTimeout(() => rej(new Error(`Leaf timed out:${id}`)), LEAF_WAIT_TIMEOUT);
|
||||
if (typeof this.leafArrivedCallbacks[id] == "undefined") {
|
||||
this.leafArrivedCallbacks[id] = [];
|
||||
}
|
||||
@@ -678,7 +704,7 @@ class LocalPouchDB {
|
||||
});
|
||||
}
|
||||
|
||||
async getDBLeaf(id: string): Promise<string> {
|
||||
async getDBLeaf(id: string, waitForReady: boolean): Promise<string> {
|
||||
// when in cache, use that.
|
||||
if (this.hashCacheRev[id]) {
|
||||
return this.hashCacheRev[id];
|
||||
@@ -700,7 +726,7 @@ class LocalPouchDB {
|
||||
}
|
||||
throw new Error(`retrive leaf, but it was not leaf.`);
|
||||
} catch (ex) {
|
||||
if (ex.status && ex.status == 404) {
|
||||
if (ex.status && ex.status == 404 && waitForReady) {
|
||||
// just leaf is not ready.
|
||||
// wait for on
|
||||
if ((await this.waitForLeafReady(id)) === false) {
|
||||
@@ -755,6 +781,10 @@ class LocalPouchDB {
|
||||
// retrieve metadata only
|
||||
if (!obj.type || (obj.type && obj.type == "notes") || obj.type == "newnote" || obj.type == "plain") {
|
||||
let note = obj as Entry;
|
||||
let children: string[] = [];
|
||||
if (obj.type == "newnote" || obj.type == "plain") {
|
||||
children = obj.children;
|
||||
}
|
||||
let doc: LoadedEntry & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta = {
|
||||
data: "",
|
||||
_id: note._id,
|
||||
@@ -764,7 +794,7 @@ class LocalPouchDB {
|
||||
_deleted: obj._deleted,
|
||||
_rev: obj._rev,
|
||||
_conflicts: obj._conflicts,
|
||||
children: [],
|
||||
children: children,
|
||||
datatype: "newnote",
|
||||
};
|
||||
return doc;
|
||||
@@ -777,7 +807,7 @@ class LocalPouchDB {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
async getDBEntry(path: string, opt?: PouchDB.Core.GetOptions, dump = false): Promise<false | LoadedEntry> {
|
||||
async getDBEntry(path: string, opt?: PouchDB.Core.GetOptions, dump = false, waitForReady = true): Promise<false | LoadedEntry> {
|
||||
let id = path2id(path);
|
||||
try {
|
||||
let obj: EntryDocResponse = null;
|
||||
@@ -827,13 +857,14 @@ class LocalPouchDB {
|
||||
}
|
||||
let childrens: string[];
|
||||
try {
|
||||
childrens = await Promise.all(obj.children.map((e) => this.getDBLeaf(e)));
|
||||
childrens = await Promise.all(obj.children.map((e) => this.getDBLeaf(e, waitForReady)));
|
||||
if (dump) {
|
||||
Logger(`childrens:`);
|
||||
Logger(childrens);
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger(`Something went wrong on reading elements of ${obj._id} from database.`, LOG_LEVEL.NOTICE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
this.corruptedEntries[obj._id] = obj;
|
||||
return false;
|
||||
}
|
||||
@@ -1140,7 +1171,7 @@ class LocalPouchDB {
|
||||
} else {
|
||||
Logger(`save failed:id:${item.id} rev:${item.rev}`, LOG_LEVEL.NOTICE);
|
||||
Logger(item);
|
||||
this.disposeHashCache();
|
||||
// this.disposeHashCache();
|
||||
saved = false;
|
||||
}
|
||||
}
|
||||
@@ -1455,6 +1486,7 @@ class LocalPouchDB {
|
||||
this.changeHandler.cancel();
|
||||
}
|
||||
await this.closeReplication();
|
||||
Logger("Database closed for reset Database.");
|
||||
this.isReady = false;
|
||||
await this.localDatabase.destroy();
|
||||
this.localDatabase = null;
|
||||
@@ -1720,7 +1752,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
name: "Dump informations of this doc ",
|
||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||
//this.replicate();
|
||||
this.localDatabase.getDBEntry(view.file.path, {}, true);
|
||||
this.localDatabase.getDBEntry(view.file.path, {}, true, false);
|
||||
},
|
||||
});
|
||||
this.addCommand({
|
||||
@@ -2161,10 +2193,16 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.refreshStatusText();
|
||||
for (var change of docs) {
|
||||
if (this.localDatabase.isSelfModified(change._id, change._rev)) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
Logger("replication change arrived", LOG_LEVEL.VERBOSE);
|
||||
if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo") {
|
||||
if (change._id.startsWith("ps:")) {
|
||||
continue;
|
||||
}
|
||||
if (change._id.startsWith("h:")) {
|
||||
continue;
|
||||
}
|
||||
if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo" && change.type != "plugin") {
|
||||
Logger("replication change arrived", LOG_LEVEL.VERBOSE);
|
||||
await this.handleDBChanged(change);
|
||||
}
|
||||
if (change.type == "versioninfo") {
|
||||
@@ -2229,7 +2267,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.statusBar.title = this.localDatabase.syncStatus;
|
||||
let waiting = "";
|
||||
if (this.settings.batchSave) {
|
||||
waiting = " " + this.batchFileChange.map((e) => "🚀").join("");
|
||||
waiting = " " + this.batchFileChange.map((e) => "🛫").join("");
|
||||
waiting = waiting.replace(/🛫{10}/g,"🚀");
|
||||
}
|
||||
this.statusBar.setText(`Sync:${w} ↑${sent} ↓${arrived}${waiting}`);
|
||||
}
|
||||
@@ -2267,7 +2306,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const filesStorage = this.app.vault.getFiles();
|
||||
const filesStorageName = filesStorage.map((e) => e.path);
|
||||
const wf = await this.localDatabase.localDatabase.allDocs();
|
||||
const filesDatabase = wf.rows.filter((e) => !e.id.startsWith("h:") && e.id != "obsydian_livesync_version").map((e) => id2path(e.id));
|
||||
const filesDatabase = wf.rows.filter((e) => !e.id.startsWith("h:") && !e.id.startsWith("ps:") && e.id != "obsydian_livesync_version").map((e) => id2path(e.id));
|
||||
|
||||
const onlyInStorage = filesStorage.filter((e) => filesDatabase.indexOf(e.path) == -1);
|
||||
const onlyInDatabase = filesDatabase.filter((e) => filesStorageName.indexOf(e) == -1);
|
||||
@@ -2325,7 +2364,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
});
|
||||
await runAll("UPDATE STORAGE", onlyInDatabase, async (e) => {
|
||||
Logger(`Pull from db:${e}`);
|
||||
await this.pullFile(e, filesStorage);
|
||||
await this.pullFile(e, filesStorage, false, null, false);
|
||||
});
|
||||
await runAll("CHECK FILE STATUS", syncFiles, async (e) => {
|
||||
await this.syncFileBetweenDBandStorage(e, filesStorage);
|
||||
@@ -2526,20 +2565,20 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}).open();
|
||||
}
|
||||
}
|
||||
async pullFile(filename: string, fileList?: TFile[], force?: boolean, rev?: string) {
|
||||
async pullFile(filename: string, fileList?: TFile[], force?: boolean, rev?: string, waitForReady: boolean = true) {
|
||||
if (!fileList) {
|
||||
fileList = this.app.vault.getFiles();
|
||||
}
|
||||
let targetFiles = fileList.filter((e) => e.path == id2path(filename));
|
||||
if (targetFiles.length == 0) {
|
||||
//have to create;
|
||||
let doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null);
|
||||
let doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
||||
if (doc === false) return;
|
||||
await this.doc2storage_create(doc, force);
|
||||
} else if (targetFiles.length == 1) {
|
||||
//normal case
|
||||
let file = targetFiles[0];
|
||||
let doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null);
|
||||
let doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
||||
if (doc === false) return;
|
||||
await this.doc2storate_modify(doc, file, force);
|
||||
} else {
|
||||
@@ -2551,18 +2590,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
async syncFileBetweenDBandStorage(file: TFile, fileList?: TFile[]) {
|
||||
let doc = await this.localDatabase.getDBEntryMeta(file.path);
|
||||
if (doc === false) return;
|
||||
|
||||
let storageMtime = ~~(file.stat.mtime / 1000);
|
||||
let docMtime = ~~(doc.mtime / 1000);
|
||||
if (storageMtime > docMtime) {
|
||||
//newer local file.
|
||||
Logger("DB -> STORAGE :" + file.path);
|
||||
Logger("STORAGE -> DB :" + file.path);
|
||||
Logger(`${storageMtime} > ${docMtime}`);
|
||||
await this.updateIntoDB(file);
|
||||
} else if (storageMtime < docMtime) {
|
||||
//newer database file.
|
||||
Logger("STORAGE <- DB :" + file.path);
|
||||
Logger(`${storageMtime} < ${docMtime}`);
|
||||
let docx = await this.localDatabase.getDBEntry(file.path);
|
||||
let docx = await this.localDatabase.getDBEntry(file.path, null, false, false);
|
||||
if (docx != false) {
|
||||
await this.doc2storate_modify(docx, file);
|
||||
}
|
||||
@@ -2595,7 +2635,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
datatype: datatype,
|
||||
};
|
||||
//From here
|
||||
let old = await this.localDatabase.getDBEntry(fullpath);
|
||||
let old = await this.localDatabase.getDBEntry(fullpath, null, false, false);
|
||||
if (old !== false) {
|
||||
let oldData = { data: old.data, deleted: old._deleted };
|
||||
let newData = { data: d.data, deleted: d._deleted };
|
||||
@@ -2975,6 +3015,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
.setButtonText("Apply and send")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.setClass("sls-btn-left")
|
||||
.onClick(async () => {
|
||||
await applyEncryption(true);
|
||||
})
|
||||
@@ -2984,6 +3025,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
.setButtonText("Apply and receive")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.setClass("sls-btn-right")
|
||||
.onClick(async () => {
|
||||
await applyEncryption(false);
|
||||
})
|
||||
@@ -3015,6 +3057,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
if (this.plugin.settings.versionUpFlash != "") {
|
||||
let c = containerEl.createEl("div", { text: this.plugin.settings.versionUpFlash });
|
||||
c.createEl("button", { text: "I got it and updated." }, (e) => {
|
||||
e.addClass("mod-cta");
|
||||
e.addEventListener("click", async () => {
|
||||
this.plugin.settings.versionUpFlash = "";
|
||||
await this.plugin.saveSettings();
|
||||
@@ -3179,6 +3222,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization, and this device was not marked as 'resolved'. it caused by some operations like this. re-initialized. Local database initialization should be required. please back your vault up, reset local database, and press 'Mark this device as resolved'. ",
|
||||
});
|
||||
c.createEl("button", { text: "I'm ready, mark this device 'resolved'" }, (e) => {
|
||||
e.addClass("mod-warning");
|
||||
e.addEventListener("click", async () => {
|
||||
await this.plugin.markRemoteResolved();
|
||||
c.remove();
|
||||
@@ -3191,6 +3235,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization. (This device is marked 'resolved') When all your devices are marked 'resolved', unlock the database.",
|
||||
});
|
||||
c.createEl("button", { text: "I'm ready, unlock the database" }, (e) => {
|
||||
e.addClass("mod-warning");
|
||||
e.addEventListener("click", async () => {
|
||||
await this.plugin.markRemoteUnlocked();
|
||||
c.remove();
|
||||
@@ -3228,6 +3273,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
.setButtonText("Drop and send")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.setClass("sls-btn-left")
|
||||
.onClick(async () => {
|
||||
await dropHistory(true);
|
||||
})
|
||||
@@ -3237,6 +3283,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
.setButtonText("Drop and receive")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.setClass("sls-btn-right")
|
||||
.onClick(async () => {
|
||||
await dropHistory(false);
|
||||
})
|
||||
@@ -3249,6 +3296,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
button
|
||||
.setButtonText("Lock")
|
||||
.setDisabled(false)
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
await this.plugin.markRemoteLocked();
|
||||
})
|
||||
@@ -3271,6 +3319,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
button
|
||||
.setButtonText("Reset")
|
||||
.setDisabled(false)
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
await this.plugin.tryResetRemoteDatabase();
|
||||
})
|
||||
@@ -3282,6 +3331,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
button
|
||||
.setButtonText("Reset")
|
||||
.setDisabled(false)
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
await this.plugin.resetLocalDatabase();
|
||||
})
|
||||
@@ -3299,6 +3349,279 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
await this.plugin.initializeDatabase();
|
||||
})
|
||||
);
|
||||
|
||||
// With great respect, thank you TfTHacker!
|
||||
// refered: https://github.com/TfTHacker/obsidian42-brat/blob/main/src/features/BetaPlugins.ts
|
||||
containerEl.createEl("h3", { text: "Plugins and settings (bleeding edge)" });
|
||||
|
||||
// new Setting(containerEl)
|
||||
// .setName("Use Plugins and settings")
|
||||
// .setDesc("It's on the bleeding edge. If you change this option, close setting dialog once,")
|
||||
// .addToggle((toggle) =>
|
||||
// toggle.setValue(this.plugin.settings.usePluginSettings).onChange(async (value) => {
|
||||
// this.plugin.settings.usePluginSettings = value;
|
||||
// await this.plugin.saveSettings();
|
||||
// })
|
||||
// );
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Show own plugins and settings")
|
||||
.setDesc("Show ")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.showOwnPlugins).onChange(async (value) => {
|
||||
this.plugin.settings.showOwnPlugins = value;
|
||||
await this.plugin.saveSettings();
|
||||
updatePluginPane();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Device and Vault name")
|
||||
.setDesc("")
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("desktop-main")
|
||||
.setValue(this.plugin.settings.deviceAndVaultName)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.deviceAndVaultName = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
// text.inputEl.setAttribute("type", "password");
|
||||
});
|
||||
|
||||
const sweepPlugin = async () => {
|
||||
// delete old database plugin entries
|
||||
// TODO: don't delete always.
|
||||
const db = this.plugin.localDatabase.localDatabase;
|
||||
let oldDocs = await db.allDocs({ startkey: `ps:${this.plugin.settings.deviceAndVaultName}-`, endkey: `ps:${this.plugin.settings.deviceAndVaultName}.`, include_docs: true });
|
||||
let delDocs = oldDocs.rows.map((e) => {
|
||||
e.doc._deleted = true;
|
||||
return e.doc;
|
||||
});
|
||||
await db.bulkDocs(delDocs);
|
||||
|
||||
// sweep current plugin.
|
||||
// @ts-ignore
|
||||
const pl = this.plugin.app.plugins;
|
||||
const manifests: PluginManifest[] = Object.values(pl.manifests);
|
||||
for (let m of manifests) {
|
||||
let path = normalizePath(m.dir) + "/";
|
||||
const adapter = this.plugin.app.vault.adapter;
|
||||
let files = ["manifest.json", "main.js", "style.css", "data.json"];
|
||||
let pluginData: { [key: string]: string } = {};
|
||||
for (let file of files) {
|
||||
let thePath = path + file;
|
||||
if (await adapter.exists(thePath)) {
|
||||
pluginData[file] = await adapter.read(thePath);
|
||||
}
|
||||
}
|
||||
let mtime = 0;
|
||||
if (await adapter.exists(path + "/data.json")) {
|
||||
mtime = (await adapter.stat(path + "/data.json")).mtime;
|
||||
}
|
||||
let p: PluginDataEntry = {
|
||||
_id: `ps:${this.plugin.settings.deviceAndVaultName}-${m.id}`,
|
||||
dataJson: pluginData["data.json"],
|
||||
deviceVaultName: this.plugin.settings.deviceAndVaultName,
|
||||
mainJs: pluginData["main.js"],
|
||||
styleCss: pluginData["style.css"],
|
||||
manifest: m,
|
||||
manifestJson: pluginData["manifest.json"],
|
||||
mtime: mtime,
|
||||
type: "plugin",
|
||||
};
|
||||
let d: LoadedEntry = {
|
||||
_id: p._id,
|
||||
data: JSON.stringify(p),
|
||||
ctime: mtime,
|
||||
mtime: mtime,
|
||||
size: 0,
|
||||
children: [],
|
||||
datatype: "plain",
|
||||
};
|
||||
await this.plugin.localDatabase.putDBEntry(d);
|
||||
}
|
||||
await this.plugin.replicate(true);
|
||||
updatePluginPane();
|
||||
};
|
||||
const updatePluginPane = async () => {
|
||||
const db = this.plugin.localDatabase.localDatabase;
|
||||
let docList = await db.allDocs<PluginDataEntry>({ startkey: `ps:`, endkey: `ps;`, include_docs: false });
|
||||
let oldDocs: PluginDataEntry[] = ((await Promise.all(docList.rows.map(async (e) => await this.plugin.localDatabase.getDBEntry(e.id)))).filter((e) => e !== false) as LoadedEntry[]).map((e) => JSON.parse(e.data));
|
||||
let plugins: { [key: string]: PluginDataEntry[] } = {};
|
||||
let allPlugins: { [key: string]: PluginDataEntry } = {};
|
||||
let thisDevicePlugins: { [key: string]: PluginDataEntry } = {};
|
||||
for (let v of oldDocs) {
|
||||
if (typeof plugins[v.deviceVaultName] === "undefined") {
|
||||
plugins[v.deviceVaultName] = [];
|
||||
}
|
||||
plugins[v.deviceVaultName].push(v);
|
||||
allPlugins[v._id] = v;
|
||||
if (v.deviceVaultName == this.plugin.settings.deviceAndVaultName) {
|
||||
thisDevicePlugins[v.manifest.id] = v;
|
||||
}
|
||||
}
|
||||
let html = `
|
||||
<div class='sls-plugins-wrap'>
|
||||
<table class='sls-plugins-tbl'>
|
||||
`;
|
||||
for (let vaults in plugins) {
|
||||
if (!this.plugin.settings.showOwnPlugins && vaults == this.plugin.settings.deviceAndVaultName) continue;
|
||||
html += `
|
||||
<tr>
|
||||
<th colspan=2>${escapeStringToHTML(vaults)}</th>
|
||||
</tr>`
|
||||
for (let v of plugins[vaults]) {
|
||||
let mtime = v.mtime == 0 ? "-" : new Date(v.mtime).toLocaleString();
|
||||
let settingApplyable: boolean | string = "-";
|
||||
let settingFleshness: string = "";
|
||||
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] && thisDevicePlugins[v.manifest.id].dataJson && v.dataJson) {
|
||||
// have this plugin.
|
||||
let localSetting = thisDevicePlugins[v.manifest.id].dataJson;
|
||||
|
||||
try {
|
||||
let remoteSetting = v.dataJson;
|
||||
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.
|
||||
let 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) => {
|
||||
console.dir("pluginData:" + e.attributes.getNamedItem("data-key").value);
|
||||
let plugin = allPlugins[e.attributes.getNamedItem("data-key").value];
|
||||
const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/";
|
||||
const adapter = this.plugin.app.vault.adapter;
|
||||
// @ts-ignore
|
||||
let stat = this.plugin.app.plugins.enabledPlugins[plugin.manifest.id];
|
||||
if (stat) {
|
||||
// @ts-ignore
|
||||
await this.plugin.app.plugins.unloadPlugin(plugin.manifest.id);
|
||||
Logger(`Unload plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE);
|
||||
}
|
||||
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.plugin.app.plugins.loadPlugin(plugin.manifest.id);
|
||||
Logger(`Load plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE);
|
||||
}
|
||||
sweepPlugin();
|
||||
})
|
||||
);
|
||||
pluginConfig.querySelectorAll(".apply-plugin-version").forEach((e) =>
|
||||
e.addEventListener("click", async (evt) => {
|
||||
console.dir("pluginVersion:" + e.attributes.getNamedItem("data-key").value);
|
||||
|
||||
let plugin = allPlugins[e.attributes.getNamedItem("data-key").value];
|
||||
|
||||
// @ts-ignore
|
||||
let stat = this.plugin.app.plugins.enabledPlugins[plugin.manifest.id];
|
||||
if (stat) {
|
||||
// @ts-ignore
|
||||
await this.plugin.app.plugins.unloadPlugin(plugin.manifest.id);
|
||||
Logger(`Unload plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE);
|
||||
}
|
||||
|
||||
const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/";
|
||||
const adapter = this.plugin.app.vault.adapter;
|
||||
if ((await adapter.exists(pluginTargetFolderPath)) === false) {
|
||||
await adapter.mkdir(pluginTargetFolderPath);
|
||||
}
|
||||
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.plugin.app.plugins.loadPlugin(plugin.manifest.id);
|
||||
Logger(`Load plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE);
|
||||
}
|
||||
sweepPlugin();
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
let pluginConfig = containerEl.createEl("div");
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Reload")
|
||||
.setDesc("Reload List")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Reload")
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
await updatePluginPane();
|
||||
})
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Save plugins into the database")
|
||||
.setDesc("Now, it wouldn't work automatically")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Save plugins")
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
if (!this.plugin.settings.encrypt) {
|
||||
Logger("You have to encrypt the database to use plugin setting sync.", LOG_LEVEL.NOTICE);
|
||||
return;
|
||||
}
|
||||
if (!this.plugin.settings.deviceAndVaultName) {
|
||||
Logger("You have to set your device and vault name.", LOG_LEVEL.NOTICE);
|
||||
return;
|
||||
}
|
||||
await sweepPlugin();
|
||||
})
|
||||
);
|
||||
updatePluginPane();
|
||||
containerEl.createEl("h3", { text: "Corrupted data" });
|
||||
|
||||
if (Object.keys(this.plugin.localDatabase.corruptedEntries).length > 0) {
|
||||
@@ -3312,6 +3635,19 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
xx.remove();
|
||||
});
|
||||
});
|
||||
ba.addClass("mod-warning")
|
||||
xx.createEl("button", { text: `Restore from file` }, (e) => {
|
||||
e.addEventListener("click", async () => {
|
||||
let f = await this.app.vault.getFiles().filter((e) => path2id(e.path) == k);
|
||||
if (f.length == 0) {
|
||||
Logger("Not found in vault", LOG_LEVEL.NOTICE);
|
||||
return;
|
||||
}
|
||||
await this.plugin.updateIntoDB(f[0]);
|
||||
xx.remove();
|
||||
});
|
||||
});
|
||||
xx.addClass("mod-warning")
|
||||
}
|
||||
} else {
|
||||
let cx = containerEl.createEl("div", { text: "There's no collupted data." });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.1.19",
|
||||
"version": "0.1.22",
|
||||
"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",
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.1.19",
|
||||
"version": "0.1.22",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.1.19",
|
||||
"version": "0.1.22",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"diff-match-patch": "^1.0.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.1.19",
|
||||
"version": "0.1.22",
|
||||
"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",
|
||||
"scripts": {
|
||||
|
||||
29
styles.css
29
styles.css
@@ -28,3 +28,32 @@
|
||||
-webkit-filter: grayscale(100%);
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
.tcenter {
|
||||
text-align: center;
|
||||
}
|
||||
.sls-plugins-wrap {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
/* overflow: scroll; */
|
||||
}
|
||||
.sls-plugins-tbl {
|
||||
border:1px solid var(--background-modifier-border);
|
||||
width: 100%;
|
||||
}
|
||||
.divider th{
|
||||
border-top:1px solid var(--background-modifier-border);
|
||||
}
|
||||
/* .sls-table-head{
|
||||
width:50%;
|
||||
}
|
||||
.sls-table-tail{
|
||||
width:50%;
|
||||
|
||||
} */
|
||||
|
||||
.sls-btn-left {
|
||||
padding-right:4px;
|
||||
}
|
||||
.sls-btn-right {
|
||||
padding-left:4px;
|
||||
}
|
||||
Reference in New Issue
Block a user