mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2025-02-07 19:30:08 +02:00
Implemented:
- History dialog Improved: - Speed up Garbage Collection.
This commit is contained in:
parent
ebcb059d99
commit
15b580aa9a
@ -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",
|
||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -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",
|
||||
|
@ -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": {
|
||||
|
132
src/DocumentHistoryModal.ts
Normal file
132
src/DocumentHistoryModal.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -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,
|
||||
});
|
||||
@ -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<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;
|
||||
|
@ -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();
|
||||
|
18
src/main.ts
18
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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
30
styles.css
30
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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user