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; +}