diff --git a/manifest.json b/manifest.json
index cffd314..3c2a103 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,7 +1,7 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
- "version": "0.5.0",
+ "version": "0.6.0",
"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",
diff --git a/package-lock.json b/package-lock.json
index 21d1b05..1aa0cd2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "obsidian-livesync",
- "version": "0.5.0",
+ "version": "0.6.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "obsidian-livesync",
- "version": "0.5.0",
+ "version": "0.6.0",
"license": "MIT",
"dependencies": {
"diff-match-patch": "^1.0.5",
diff --git a/package.json b/package.json
index 0ee336a..0e87a45 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "obsidian-livesync",
- "version": "0.5.0",
+ "version": "0.6.0",
"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": {
diff --git a/src/DocumentHistoryModal.ts b/src/DocumentHistoryModal.ts
new file mode 100644
index 0000000..4508c21
--- /dev/null
+++ b/src/DocumentHistoryModal.ts
@@ -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
(${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 += "" + escapeStringToHTML(x2) + "";
+ } else if (x1 == DIFF_EQUAL) {
+ result += "" + escapeStringToHTML(x2) + "";
+ } else if (x1 == DIFF_INSERT) {
+ result += "" + escapeStringToHTML(x2) + "";
+ }
+ }
+
+ result = result.replace(/\n/g, "
");
+ } 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();
+ }
+}
diff --git a/src/LocalPouchDB.ts b/src/LocalPouchDB.ts
index b45ca4e..c86dbe1 100644
--- a/src/LocalPouchDB.ts
+++ b/src/LocalPouchDB.ts
@@ -121,7 +121,7 @@ export class LocalPouchDB {
this.changeHandler = this.cancelHandler(this.changeHandler);
this.localDatabase = null;
this.localDatabase = new PouchDB(this.dbname + "-livesync", {
- auto_compaction: true,
+ auto_compaction: this.settings.useHistory ? false : true,
revs_limit: 100,
deterministic_revs: true,
});
@@ -1204,7 +1204,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 +1224,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(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(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;
diff --git a/src/ObsidianLiveSyncSettingTab.ts b/src/ObsidianLiveSyncSettingTab.ts
index f034e03..7b60add 100644
--- a/src/ObsidianLiveSyncSettingTab.ts
+++ b/src/ObsidianLiveSyncSettingTab.ts
@@ -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();
diff --git a/src/main.ts b/src/main.ts
index 8c3f4ba..0d66760 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -24,6 +24,7 @@ import { LocalPouchDB } from "./LocalPouchDB";
import { LogDisplayModal } from "./LogDisplayModal";
import { ConflictResolveModal } from "./ConflictResolveModal";
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
+import { DocumentHistoryModal } from "./DocumentHistoryModal";
export default class ObsidianLiveSyncPlugin extends Plugin {
settings: ObsidianLiveSyncSettings;
@@ -45,6 +46,13 @@ 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");
@@ -203,6 +211,13 @@ 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(() => {
@@ -256,6 +271,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
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);
@@ -791,7 +807,7 @@ 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);
}
diff --git a/src/types.ts b/src/types.ts
index fd15c7a..f07beeb 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -58,6 +58,7 @@ export interface ObsidianLiveSyncSettings {
checkIntegrityOnSave: boolean;
batch_size: number;
batches_limit: number;
+ useHistory:boolean;
}
export const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
@@ -98,6 +99,7 @@ export const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
checkIntegrityOnSave: false,
batch_size: 250,
batches_limit: 40,
+ useHistory:false,
};
export const PERIODIC_PLUGIN_SWEEP = 60;
diff --git a/styles.css b/styles.css
index 86497ac..6a32cfe 100644
--- a/styles.css
+++ b/styles.css
@@ -140,3 +140,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;
+}