diff --git a/src/DocumentHistoryModal.ts b/src/DocumentHistoryModal.ts index 62752d6..a03746a 100644 --- a/src/DocumentHistoryModal.ts +++ b/src/DocumentHistoryModal.ts @@ -1,9 +1,9 @@ import { TFile, Modal, App } from "obsidian"; import { path2id } from "./utils"; -import { escapeStringToHTML } from "./lib/src/utils"; +import { base64ToArrayBuffer, base64ToString, escapeStringToHTML, isValidPath } from "./lib/src/utils"; import ObsidianLiveSyncPlugin from "./main"; import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch"; -import { LOG_LEVEL } from "./lib/src/types"; +import { LoadedEntry, LOG_LEVEL } from "./lib/src/types"; import { Logger } from "./lib/src/logger"; export class DocumentHistoryModal extends Modal { @@ -17,12 +17,13 @@ export class DocumentHistoryModal extends Modal { file: string; revs_info: PouchDB.Core.RevisionInfo[] = []; + currentDoc: LoadedEntry; currentText = ""; - constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile) { + constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile | string) { super(app); this.plugin = plugin; - this.file = file.path; + this.file = (file instanceof TFile) ? file.path : file; if (localStorage.getItem("ols-history-highlightdiff") == "1") { this.showDiff = true; } @@ -47,9 +48,12 @@ export class DocumentHistoryModal extends Modal { this.info.innerHTML = ""; this.contentView.innerHTML = `Could not read this revision
(${rev.rev})`; } else { + this.currentDoc = w; this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`; let result = ""; - this.currentText = w.data; + const w1data = w.datatype == "plain" ? w.data : base64ToString(w.data); + + this.currentText = w1data; 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) { @@ -57,7 +61,8 @@ export class DocumentHistoryModal extends Modal { 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); + const w2data = w2.datatype == "plain" ? w2.data : base64ToString(w2.data); + const diff = dmp.diff_main(w2data, w1data); dmp.diff_cleanupSemantic(diff); for (const v of diff) { const x1 = v[0]; @@ -73,13 +78,13 @@ export class DocumentHistoryModal extends Modal { result = result.replace(/\n/g, "
"); } else { - result = escapeStringToHTML(w.data); + result = escapeStringToHTML(w1data); } } else { - result = escapeStringToHTML(w.data); + result = escapeStringToHTML(w1data); } } else { - result = escapeStringToHTML(w.data); + result = escapeStringToHTML(w1data); } this.contentView.innerHTML = result; } @@ -138,6 +143,25 @@ export class DocumentHistoryModal extends Modal { Logger(`Old content copied to clipboard`, LOG_LEVEL.NOTICE); }); }); + buttons.createEl("button", { text: "Back to this revision" }, (e) => { + e.addClass("mod-cta"); + e.addEventListener("click", async () => { + const pathToWrite = this.file.startsWith("i:") ? this.file.substring("i:".length) : this.file; + if (!isValidPath(pathToWrite)) { + Logger("Path is not vaild to write content.", LOG_LEVEL.INFO); + } + if (this.currentDoc?.datatype == "plain") { + await this.app.vault.adapter.write(pathToWrite, this.currentDoc.data); + this.close(); + } else if (this.currentDoc?.datatype == "newnote") { + await this.app.vault.adapter.writeBinary(pathToWrite, base64ToArrayBuffer(this.currentDoc.data)); + this.close(); + } else { + + Logger(`Could not parse entry`, LOG_LEVEL.NOTICE); + } + }); + }); } onClose() { const { contentEl } = this; diff --git a/src/main.ts b/src/main.ts index e528f33..14580a0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -29,12 +29,14 @@ import { DocumentHistoryModal } from "./DocumentHistoryModal"; //@ts-ignore import PluginPane from "./PluginPane.svelte"; + import { clearAllPeriodic, clearAllTriggers, disposeMemoObject, id2path, memoIfNotExist, memoObject, path2id, retriveMemoObject, setTrigger } from "./utils"; import { decrypt, encrypt } from "./lib/src/e2ee_v2"; const isDebug = false; setNoticeClass(Notice); + class PluginDialogModal extends Modal { plugin: ObsidianLiveSyncPlugin; logEl: HTMLDivElement; @@ -118,19 +120,24 @@ class InputStringDialog extends Modal { } } } -class PopoverYesNo extends FuzzySuggestModal { +class PopoverSelectString extends FuzzySuggestModal { app: App; callback: (e: string) => void = () => { }; + getItemsFun: () => string[] = () => { + return ["yes", "no"]; - constructor(app: App, note: string, callback: (e: string) => void) { + } + + constructor(app: App, note: string, placeholder: string | null, getItemsFun: () => string[], callback: (e: string) => void) { super(app); this.app = app; - this.setPlaceholder("y/n) " + note); + this.setPlaceholder(placeholder ?? "y/n) " + note); + if (getItemsFun) this.getItemsFun = getItemsFun; this.callback = callback; } getItems(): string[] { - return ["yes", "no"]; + return this.getItemsFun(); } getItemText(item: string): string { @@ -153,11 +160,20 @@ class PopoverYesNo extends FuzzySuggestModal { const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => { return new Promise((res) => { - const popover = new PopoverYesNo(app, message, (result) => res(result as "yes" | "no")); + const popover = new PopoverSelectString(app, message, null, null, (result) => res(result as "yes" | "no")); popover.open(); }); }; +const askSelectString = (app: App, message: string, items: string[]): Promise => { + const getItemsFun = () => items; + return new Promise((res) => { + const popover = new PopoverSelectString(app, message, "Select file)", getItemsFun, (result) => res(result)); + popover.open(); + }); +}; + + const askString = (app: App, title: string, key: string, placeholder: string): Promise => { return new Promise((res) => { const dialog = new InputStringDialog(app, title, key, placeholder, (result) => res(result)); @@ -207,7 +223,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin { return false; } - showHistory(file: TFile) { + showHistory(file: TFile | string) { if (!this.settings.useHistory) { Logger("You have to enable Use History in misc.", LOG_LEVEL.NOTICE); } else { @@ -215,6 +231,29 @@ export default class ObsidianLiveSyncPlugin extends Plugin { } } + async fileHistory() { + const pageLimit = 2500; + let nextKey = ""; + const notes = []; + do { + const docs = await this.localDatabase.localDatabase.allDocs({ limit: pageLimit, startkey: nextKey, include_docs: true }); + nextKey = ""; + for (const row of docs.rows) { + const doc = row.doc; + nextKey = `${row.id}\u{10ffff}`; + if (!("type" in doc)) continue; + if (doc.type == "newnote" || doc.type == "plain") { + // const docId = doc._id.startsWith("i:") ? doc._id.substring("i:".length) : doc._id; + notes.push(id2path(doc._id)) + } + } + } while (nextKey != ""); + const target = await askSelectString(this.app, "File to view History", notes); + if (target) { + this.showHistory(target); + } + } + async onload() { setLogger(this.addLog.bind(this)); // Logger moved to global. Logger("loading plugin"); @@ -499,6 +538,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin { this.showHistory(view.file); }, }); + this.triggerRealizeSettingSyncMode = debounce(this.triggerRealizeSettingSyncMode.bind(this), 1000); this.triggerCheckPluginUpdate = debounce(this.triggerCheckPluginUpdate.bind(this), 3000); setLockNotifier(() => { @@ -519,6 +559,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin { this.syncInternalFilesAndDatabase("safe", true); }, }); + this.addCommand({ + id: "livesync-filehistory", + name: "Pick file to show history", + callback: () => { + this.fileHistory(); + }, + }) + } pluginDialog: PluginDialogModal = null; @@ -1511,7 +1559,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin { Logger("Updating database by new files"); this.setStatusBarText(`UPDATE DATABASE`); - const runAll = async (procedurename: string, objects: T[], callback: (arg: T) => Promise) => { + const runAll = async(procedurename: string, objects: T[], callback: (arg: T) => Promise) => { const count = objects.length; Logger(procedurename); let i = 0; @@ -1547,6 +1595,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin { await p.all(); Logger(`${procedurename} done.`); }; + await runAll("UPDATE DATABASE", onlyInStorage, async (e) => { Logger(`Update into ${e.path}`); @@ -1575,7 +1624,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin { } this.setStatusBarText(`NOW TRACKING!`); - Logger("Initialized,NOW TRACKING!"); + Logger("Initialized, NOW TRACKING!"); if (!isInitialized) { await (this.localDatabase.kvDB.set("initialized", true)) } @@ -2157,30 +2206,39 @@ export default class ObsidianLiveSyncPlugin extends Plugin { async getFiles( path: string, ignoreList: string[], - filter: RegExp[] + filter: RegExp[], + ignoreFilter: RegExp[], ) { + const w = await this.app.vault.adapter.list(path); let files = [ ...w.files .filter((e) => !ignoreList.some((ee) => e.endsWith(ee))) - .filter((e) => !filter || filter.some((ee) => e.match(ee))), + .filter((e) => !filter || filter.some((ee) => e.match(ee))) + .filter((e) => !ignoreFilter || ignoreFilter.every((ee) => !e.match(ee))), ]; + L1: for (const v of w.folders) { for (const ignore of ignoreList) { if (v.endsWith(ignore)) { continue L1; } } - files = files.concat(await this.getFiles(v, ignoreList, filter)); + if (ignoreFilter && ignoreFilter.some(e => v.match(e))) { + continue L1; + } + files = files.concat(await this.getFiles(v, ignoreList, filter, ignoreFilter)); } return files; } async scanInternalFiles(): Promise { - const ignoreFiles = ["node_modules", ".git", "obsidian-pouch"]; + const ignoreFilter = this.settings.syncInternalFilesIgnorePatterns.toLocaleLowerCase() + .replace(/\n| /g, "") + .split(",").filter(e => e).map(e => new RegExp(e)); const root = this.app.vault.getRoot(); const findRoot = root.path; - const filenames = (await this.getFiles(findRoot, ignoreFiles, null)).filter(e => e.startsWith(".")).filter(e => !e.startsWith(".trash")); + const filenames = (await this.getFiles(findRoot, [], null, ignoreFilter)).filter(e => e.startsWith(".")).filter(e => !e.startsWith(".trash")); const files = filenames.map(async e => { return { path: e, @@ -2344,10 +2402,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin { const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns.toLocaleLowerCase() .replace(/\n| /g, "") .split(",").filter(e => e).map(e => new RegExp(e)); - // const files = await this.scanInternalFiles(); return files.filter(file => !ignorePatterns.some(e => file.path.match(e))).filter(file => !targetFiles || (targetFiles && targetFiles.indexOf(file.path) !== -1)) - //if (ignorePatterns.some(e => filename.match(e))) continue; - //if (targetFiles !== false && targetFiles.indexOf(filename) == -1) continue; } async applyMTimeToFile(file: InternalFileInfo) {