mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-06-10 08:20:13 +00:00
656 lines
26 KiB
TypeScript
656 lines
26 KiB
TypeScript
import { TFile, Modal, App, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "../../../deps.ts";
|
|
import { getPathFromTFile, isValidPath } from "../../../common/utils.ts";
|
|
import { decodeBinary, readString } from "../../../lib/src/string_and_binary/convert.ts";
|
|
import ObsidianLiveSyncPlugin from "../../../main.ts";
|
|
import {
|
|
type DocumentID,
|
|
type FilePathWithPrefix,
|
|
type LoadedEntry,
|
|
LOG_LEVEL_INFO,
|
|
LOG_LEVEL_NOTICE,
|
|
LOG_LEVEL_VERBOSE,
|
|
} from "../../../lib/src/common/types.ts";
|
|
import { Logger } from "../../../lib/src/common/logger.ts";
|
|
import { isErrorOfMissingDoc } from "../../../lib/src/pouchdb/utils_couchdb.ts";
|
|
import { fireAndForget, getDocData, readContent } from "../../../lib/src/common/utils.ts";
|
|
import { isPlainText, stripPrefix } from "../../../lib/src/string_and_binary/path.ts";
|
|
import { scheduleOnceIfDuplicated } from "octagonal-wheels/concurrency/lock";
|
|
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
|
|
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
|
|
|
function isImage(path: string) {
|
|
const ext = path.split(".").splice(-1)[0].toLowerCase();
|
|
return ["png", "jpg", "jpeg", "gif", "bmp", "webp"].includes(ext);
|
|
}
|
|
function isComparableText(path: string) {
|
|
const ext = path.split(".").splice(-1)[0].toLowerCase();
|
|
return isPlainText(path) || ["md", "mdx", "txt", "json"].includes(ext);
|
|
}
|
|
function isComparableTextDecode(path: string) {
|
|
const ext = path.split(".").splice(-1)[0].toLowerCase();
|
|
return ["json"].includes(ext);
|
|
}
|
|
function readDocument(w: LoadedEntry) {
|
|
if (w.data.length == 0) return "";
|
|
if (isImage(w.path)) {
|
|
return new Uint8Array(decodeBinary(w.data));
|
|
}
|
|
if (w.type == "plain" || w.datatype == "plain") return getDocData(w.data);
|
|
if (isComparableTextDecode(w.path)) return readString(new Uint8Array(decodeBinary(w.data)));
|
|
if (isComparableText(w.path)) return getDocData(w.data);
|
|
try {
|
|
return readString(new Uint8Array(decodeBinary(w.data)));
|
|
} catch (ex) {
|
|
Logger(ex, LOG_LEVEL_VERBOSE);
|
|
// NO OP.
|
|
}
|
|
return getDocData(w.data);
|
|
}
|
|
export class DocumentHistoryModal extends Modal {
|
|
plugin: ObsidianLiveSyncPlugin;
|
|
core: LiveSyncBaseCore;
|
|
get services() {
|
|
return this.core.services;
|
|
}
|
|
range!: HTMLInputElement;
|
|
contentView!: HTMLDivElement;
|
|
info!: HTMLDivElement;
|
|
fileInfo!: HTMLDivElement;
|
|
showDiff = false;
|
|
diffOnly = false;
|
|
id?: DocumentID;
|
|
|
|
file: FilePathWithPrefix;
|
|
|
|
revs_info: PouchDB.Core.RevisionInfo[] = [];
|
|
currentDoc?: LoadedEntry;
|
|
currentText = "";
|
|
currentDeleted = false;
|
|
initialRev?: string;
|
|
|
|
// Diff navigation state
|
|
currentDiffIndex = -1;
|
|
diffNavContainer!: HTMLDivElement;
|
|
diffNavIndicator!: HTMLSpanElement;
|
|
diffOnlyLabel!: HTMLLabelElement;
|
|
|
|
// Search state
|
|
searchKeyword = "";
|
|
searchResults: { rev: string; index: number; matchType: "Content" | "Diff" }[] = [];
|
|
currentSearchIndex = -1;
|
|
searchResultIndicator!: HTMLSpanElement;
|
|
searchProgressIndicator!: HTMLSpanElement;
|
|
searchTimeout: number | null = null;
|
|
|
|
constructor(
|
|
app: App,
|
|
core: LiveSyncBaseCore,
|
|
plugin: ObsidianLiveSyncPlugin,
|
|
file: TFile | FilePathWithPrefix,
|
|
id?: DocumentID,
|
|
revision?: string
|
|
) {
|
|
super(app);
|
|
this.plugin = plugin;
|
|
this.core = core;
|
|
this.file = file instanceof TFile ? getPathFromTFile(file) : file;
|
|
this.id = id;
|
|
this.initialRev = revision;
|
|
if (!file && id) {
|
|
this.file = this.services.path.id2path(id);
|
|
}
|
|
if (this.app.loadLocalStorage("ols-history-highlightdiff") == "1") {
|
|
this.showDiff = true;
|
|
}
|
|
if (this.app.loadLocalStorage("ols-history-diffonly") == "1") {
|
|
this.diffOnly = true;
|
|
}
|
|
}
|
|
|
|
async loadFile(initialRev?: string) {
|
|
if (!this.id) {
|
|
this.id = await this.services.path.path2id(this.file);
|
|
}
|
|
const db = this.core.localDatabase;
|
|
try {
|
|
const w = await db.getRaw(this.id, { revs_info: true });
|
|
this.revs_info = w._revs_info?.filter((e) => e?.status == "available") ?? [];
|
|
this.range.max = `${Math.max(this.revs_info.length - 1, 0)}`;
|
|
this.range.value = this.range.max;
|
|
this.fileInfo.setText(`${this.file} / ${this.revs_info.length} revisions`);
|
|
await this.loadRevs(initialRev);
|
|
} catch (ex) {
|
|
if (isErrorOfMissingDoc(ex)) {
|
|
this.range.max = "0";
|
|
this.range.value = "";
|
|
this.range.disabled = true;
|
|
this.contentView.setText(`We don't have any history for this note.`);
|
|
} else {
|
|
this.contentView.setText(`Error while loading file.`);
|
|
Logger(ex, LOG_LEVEL_VERBOSE);
|
|
}
|
|
}
|
|
}
|
|
async loadRevs(initialRev?: string) {
|
|
if (this.revs_info.length == 0) return;
|
|
if (initialRev) {
|
|
const rIndex = this.revs_info.findIndex((e) => e.rev == initialRev);
|
|
if (rIndex >= 0) {
|
|
this.range.value = `${this.revs_info.length - 1 - rIndex}`;
|
|
}
|
|
}
|
|
const index = this.revs_info.length - 1 - (this.range.value as any) / 1;
|
|
const rev = this.revs_info[index];
|
|
await this.showExactRev(rev.rev);
|
|
}
|
|
BlobURLs = new Map<string, string>();
|
|
|
|
revokeURL(key: string) {
|
|
const v = this.BlobURLs.get(key);
|
|
if (v) {
|
|
URL.revokeObjectURL(v);
|
|
}
|
|
this.BlobURLs.delete(key);
|
|
}
|
|
generateBlobURL(key: string, data: Uint8Array<ArrayBuffer>) {
|
|
this.revokeURL(key);
|
|
const v = URL.createObjectURL(new Blob([data], { endings: "transparent", type: "application/octet-stream" }));
|
|
this.BlobURLs.set(key, v);
|
|
return v;
|
|
}
|
|
|
|
prepareContentView(usePreformatted = true) {
|
|
this.contentView.empty();
|
|
this.contentView.toggleClass("op-pre", usePreformatted);
|
|
}
|
|
|
|
appendTextDiff(diff: [number, string][]) {
|
|
let hasOmitted = false;
|
|
for (const [operation, text] of diff) {
|
|
if (operation == DIFF_DELETE) {
|
|
this.appendSearchHighlightedText(this.contentView.createSpan({ cls: "history-deleted" }), text);
|
|
hasOmitted = false;
|
|
} else if (operation == DIFF_EQUAL) {
|
|
if (this.diffOnly) {
|
|
if (!hasOmitted) {
|
|
this.contentView.appendText("\n...\n");
|
|
hasOmitted = true;
|
|
}
|
|
} else {
|
|
this.appendSearchHighlightedText(this.contentView.createSpan({ cls: "history-normal" }), text);
|
|
}
|
|
} else if (operation == DIFF_INSERT) {
|
|
this.appendSearchHighlightedText(this.contentView.createSpan({ cls: "history-added" }), text);
|
|
hasOmitted = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
appendSearchHighlightedText(container: HTMLElement, text: string) {
|
|
if (!this.searchKeyword) {
|
|
container.appendText(text);
|
|
return;
|
|
}
|
|
const escapedKeyword = this.searchKeyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
const regex = new RegExp(escapedKeyword, "gi");
|
|
let lastIndex = 0;
|
|
for (const match of text.matchAll(regex)) {
|
|
const index = match.index ?? 0;
|
|
if (index > lastIndex) {
|
|
container.appendText(text.slice(lastIndex, index));
|
|
}
|
|
container.createEl("mark", { text: match[0] });
|
|
lastIndex = index + match[0].length;
|
|
}
|
|
if (lastIndex < text.length) {
|
|
container.appendText(text.slice(lastIndex));
|
|
}
|
|
}
|
|
|
|
appendImageDiff(baseSrc: string, overlaySrc?: string) {
|
|
const wrap = this.contentView.createDiv({ cls: "ls-imgdiff-wrap" });
|
|
const overlay = wrap.createDiv({ cls: "overlay" });
|
|
overlay.createEl("img", { cls: "img-base" }, (img) => {
|
|
img.src = baseSrc;
|
|
});
|
|
if (overlaySrc) {
|
|
overlay.createEl("img", { cls: "img-overlay" }, (img) => {
|
|
img.src = overlaySrc;
|
|
});
|
|
}
|
|
}
|
|
|
|
appendDeletedNotice(usePreformatted = true) {
|
|
const notice = "(At this revision, the file has been deleted)";
|
|
if (usePreformatted) {
|
|
this.contentView.appendText(`${notice}\n`);
|
|
} else {
|
|
this.contentView.createDiv({ text: notice });
|
|
}
|
|
}
|
|
|
|
async showExactRev(rev: string) {
|
|
const db = this.core.localDatabase;
|
|
const w = await db.getDBEntry(this.file, { rev: rev }, false, false, true);
|
|
this.currentText = "";
|
|
this.currentDeleted = false;
|
|
this.prepareContentView();
|
|
if (w === false) {
|
|
this.currentDeleted = true;
|
|
this.info.empty();
|
|
this.contentView.appendText("Could not read this revision");
|
|
this.contentView.createEl("br");
|
|
this.contentView.appendText(`(${rev})`);
|
|
} else {
|
|
this.currentDoc = w;
|
|
this.info.setText(`Modified:${new Date(w.mtime).toLocaleString()}`);
|
|
const w1data = readDocument(w);
|
|
this.currentDeleted = !!w.deleted;
|
|
if (typeof w1data == "string") {
|
|
this.currentText = w1data;
|
|
}
|
|
let rendered = false;
|
|
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(this.file, { rev: oldRev }, false, false, true);
|
|
if (w2 != false) {
|
|
if (typeof w1data == "string") {
|
|
const w2data = readDocument(w2);
|
|
if (typeof w2data == "string") {
|
|
const dmp = new diff_match_patch();
|
|
const diff = dmp.diff_main(w2data, w1data);
|
|
dmp.diff_cleanupSemantic(diff);
|
|
if (this.currentDeleted) {
|
|
this.appendDeletedNotice();
|
|
}
|
|
this.appendTextDiff(diff);
|
|
rendered = true;
|
|
}
|
|
} else if (isImage(this.file)) {
|
|
const src = this.generateBlobURL("base", w1data);
|
|
const overlay = this.generateBlobURL(
|
|
"overlay",
|
|
readDocument(w2) as Uint8Array<ArrayBuffer>
|
|
);
|
|
this.prepareContentView(false);
|
|
if (this.currentDeleted) {
|
|
this.appendDeletedNotice(false);
|
|
}
|
|
this.appendImageDiff(src, overlay);
|
|
rendered = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!rendered) {
|
|
if (typeof w1data != "string") {
|
|
if (isImage(this.file)) {
|
|
const src = this.generateBlobURL("base", w1data);
|
|
this.prepareContentView(false);
|
|
if (this.currentDeleted) {
|
|
this.appendDeletedNotice(false);
|
|
}
|
|
this.appendImageDiff(src);
|
|
} else {
|
|
if (this.currentDeleted) {
|
|
this.appendDeletedNotice();
|
|
}
|
|
this.contentView.appendText("Binary file");
|
|
}
|
|
} else {
|
|
if (this.currentDeleted) {
|
|
this.appendDeletedNotice();
|
|
}
|
|
this.appendSearchHighlightedText(this.contentView, w1data);
|
|
}
|
|
}
|
|
}
|
|
// Reset diff navigation after content changes
|
|
this.resetDiffNavigation();
|
|
if (this.showDiff) {
|
|
this.navigateDiff("next");
|
|
} else if (this.searchKeyword) {
|
|
const firstMark = this.contentView.querySelector("mark");
|
|
if (firstMark) {
|
|
firstMark.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Navigate to the previous or next diff block in the content view.
|
|
* Only effective when diff highlighting is enabled.
|
|
*/
|
|
navigateDiff(direction: "prev" | "next") {
|
|
const diffElements = this.contentView.querySelectorAll(".history-added, .history-deleted");
|
|
if (diffElements.length === 0) return;
|
|
|
|
// Remove previous focus highlight
|
|
const prevFocused = this.contentView.querySelector(".diff-focused");
|
|
if (prevFocused) {
|
|
prevFocused.classList.remove("diff-focused");
|
|
}
|
|
|
|
if (direction === "next") {
|
|
this.currentDiffIndex = (this.currentDiffIndex + 1) % diffElements.length;
|
|
} else {
|
|
this.currentDiffIndex = this.currentDiffIndex <= 0 ? diffElements.length - 1 : this.currentDiffIndex - 1;
|
|
}
|
|
|
|
const target = diffElements[this.currentDiffIndex];
|
|
target.classList.add("diff-focused");
|
|
target.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
|
|
this.diffNavIndicator.setText(`${this.currentDiffIndex + 1}/${diffElements.length}`);
|
|
}
|
|
|
|
/**
|
|
* Reset the diff navigation index and update the indicator.
|
|
*/
|
|
resetDiffNavigation() {
|
|
this.currentDiffIndex = -1;
|
|
if (this.diffNavIndicator) {
|
|
if (this.showDiff) {
|
|
const diffElements = this.contentView.querySelectorAll(".history-added, .history-deleted");
|
|
this.diffNavIndicator.setText(diffElements.length > 0 ? `0/${diffElements.length}` : "\u2014");
|
|
} else {
|
|
this.diffNavIndicator.setText("\u2014");
|
|
}
|
|
}
|
|
this.updateDiffNavVisibility();
|
|
}
|
|
|
|
/**
|
|
* Show or hide the diff navigation buttons based on the showDiff state.
|
|
*/
|
|
updateDiffNavVisibility() {
|
|
if (this.diffNavContainer) {
|
|
this.diffNavContainer.style.display = this.showDiff ? "flex" : "none";
|
|
}
|
|
if (this.diffOnlyLabel) {
|
|
this.diffOnlyLabel.style.display = this.showDiff ? "inline-block" : "none";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Search through the last 100 revisions for the given keyword.
|
|
*/
|
|
async performSearch(keyword: string) {
|
|
this.searchKeyword = keyword;
|
|
this.searchResults = [];
|
|
this.currentSearchIndex = -1;
|
|
|
|
if (!keyword) {
|
|
this.searchResultIndicator.setText("");
|
|
this.searchProgressIndicator.setText("");
|
|
return;
|
|
}
|
|
|
|
const db = this.core.localDatabase;
|
|
const limit = 100;
|
|
const totalRevs = this.revs_info.length;
|
|
const end = Math.min(totalRevs, limit);
|
|
|
|
this.searchProgressIndicator.setText("Searching...");
|
|
|
|
const dmp = new diff_match_patch();
|
|
|
|
// 0 is the newest, higher index is older.
|
|
for (let i = 0; i < end; i++) {
|
|
const revInfo = this.revs_info[i];
|
|
const rev = revInfo.rev;
|
|
|
|
this.searchProgressIndicator.setText(`Searching ${i + 1}/${end}...`);
|
|
|
|
const doc = await db.getDBEntry(this.file, { rev: rev }, false, false, true);
|
|
if (doc === false) continue;
|
|
|
|
const content = readDocument(doc);
|
|
if (typeof content !== "string") continue;
|
|
|
|
const keywordLower = keyword.toLocaleLowerCase();
|
|
|
|
// Search in content
|
|
if (content.toLocaleLowerCase().includes(keywordLower)) {
|
|
this.searchResults.push({ rev, index: i, matchType: "Content" });
|
|
this.updateSearchUI();
|
|
continue;
|
|
}
|
|
|
|
// Search in diff (from older version to this version)
|
|
// Older version is at i + 1
|
|
if (i < totalRevs - 1) {
|
|
const olderRev = this.revs_info[i + 1].rev;
|
|
const olderDoc = await db.getDBEntry(this.file, { rev: olderRev }, false, false, true);
|
|
if (olderDoc !== false) {
|
|
const olderContent = readDocument(olderDoc);
|
|
if (typeof olderContent === "string") {
|
|
const diffs = dmp.diff_main(olderContent, content);
|
|
let foundInDiff = false;
|
|
for (const d of diffs) {
|
|
if (
|
|
(d[0] === DIFF_INSERT || d[0] === DIFF_DELETE) &&
|
|
d[1].toLocaleLowerCase().includes(keywordLower)
|
|
) {
|
|
foundInDiff = true;
|
|
break;
|
|
}
|
|
}
|
|
if (foundInDiff) {
|
|
this.searchResults.push({ rev, index: i, matchType: "Diff" });
|
|
this.updateSearchUI();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this.searchProgressIndicator.setText("Done");
|
|
this.updateSearchUI();
|
|
}
|
|
|
|
updateSearchUI() {
|
|
if (this.searchResults.length === 0) {
|
|
this.searchResultIndicator.setText(this.searchKeyword ? "No matches found" : "");
|
|
} else {
|
|
const current = this.currentSearchIndex >= 0 ? this.currentSearchIndex + 1 : 0;
|
|
this.searchResultIndicator.setText(`${current}/${this.searchResults.length} matches`);
|
|
}
|
|
}
|
|
|
|
navigateSearch(direction: "prev" | "next") {
|
|
if (this.searchResults.length === 0) return;
|
|
|
|
if (direction === "next") {
|
|
this.currentSearchIndex = (this.currentSearchIndex + 1) % this.searchResults.length;
|
|
} else {
|
|
this.currentSearchIndex =
|
|
this.currentSearchIndex <= 0 ? this.searchResults.length - 1 : this.currentSearchIndex - 1;
|
|
}
|
|
|
|
const match = this.searchResults[this.currentSearchIndex];
|
|
this.range.value = `${this.revs_info.length - 1 - match.index}`;
|
|
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
|
this.updateSearchUI();
|
|
|
|
// If it's a diff match, make sure Highlight diff is on
|
|
if (match.matchType === "Diff" && !this.showDiff) {
|
|
// We could auto-enable it, but maybe just notify the user?
|
|
// For now, let's just let the user toggle it if they want to see the diff.
|
|
}
|
|
}
|
|
|
|
override onOpen() {
|
|
const { contentEl } = this;
|
|
this.titleEl.setText("Document History");
|
|
contentEl.empty();
|
|
this.fileInfo = contentEl.createDiv("");
|
|
this.fileInfo.addClass("op-info");
|
|
|
|
// Search Row
|
|
const searchRow = contentEl.createDiv("");
|
|
searchRow.addClass("op-info");
|
|
searchRow.addClass("search-row");
|
|
searchRow.addClass("history-search-row");
|
|
|
|
const searchInput = searchRow.createEl("input", {
|
|
type: "text",
|
|
placeholder: "Search in history (last 100)...",
|
|
});
|
|
searchInput.addClass("history-search-input");
|
|
searchInput.addEventListener("input", () => {
|
|
if (this.searchTimeout) {
|
|
compatGlobal.clearTimeout(this.searchTimeout);
|
|
}
|
|
this.searchTimeout = compatGlobal.setTimeout(() => {
|
|
void this.performSearch(searchInput.value);
|
|
}, 500);
|
|
});
|
|
|
|
searchRow.createEl("button", { text: "\u25B2" }, (e) => {
|
|
e.title = "Previous match";
|
|
e.addEventListener("click", () => this.navigateSearch("prev"));
|
|
});
|
|
searchRow.createEl("button", { text: "\u25BC" }, (e) => {
|
|
e.title = "Next match";
|
|
e.addEventListener("click", () => this.navigateSearch("next"));
|
|
});
|
|
|
|
this.searchResultIndicator = searchRow.createEl("span", { text: "" });
|
|
this.searchResultIndicator.addClass("history-search-result-indicator");
|
|
|
|
this.searchProgressIndicator = searchRow.createEl("span", { text: "" });
|
|
this.searchProgressIndicator.addClass("history-search-progress-indicator");
|
|
|
|
const divView = contentEl.createDiv("");
|
|
divView.addClass("op-flex");
|
|
|
|
divView.createEl("input", { type: "range" }, (e) => {
|
|
this.range = e;
|
|
e.addEventListener("change", (e) => {
|
|
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
|
});
|
|
e.addEventListener("input", (e) => {
|
|
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
|
});
|
|
});
|
|
const diffOptionsRow = contentEl.createDiv("");
|
|
diffOptionsRow.addClass("op-info");
|
|
diffOptionsRow.addClass("diff-options-row");
|
|
diffOptionsRow.addClass("history-diff-options-row");
|
|
|
|
const highlightDiffContainer = diffOptionsRow.createDiv("");
|
|
highlightDiffContainer.addClass("history-highlight-diff-container");
|
|
|
|
highlightDiffContainer.createEl("label", {}, (label) => {
|
|
label.addClass("history-highlight-diff-label");
|
|
label.createEl("input", { type: "checkbox" }, (checkbox) => {
|
|
if (this.showDiff) {
|
|
checkbox.checked = true;
|
|
}
|
|
checkbox.addEventListener("input", (evt: any) => {
|
|
this.showDiff = checkbox.checked;
|
|
this.app.saveLocalStorage("ols-history-highlightdiff", this.showDiff == true ? "1" : null);
|
|
this.updateDiffNavVisibility();
|
|
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
|
});
|
|
});
|
|
label.appendText("Highlight diff");
|
|
});
|
|
|
|
const diffOnlyLabel = diffOptionsRow.createEl("label", {});
|
|
diffOnlyLabel.createEl("input", { type: "checkbox" }, (checkbox) => {
|
|
if (this.diffOnly) {
|
|
checkbox.checked = true;
|
|
}
|
|
checkbox.addEventListener("input", (evt: any) => {
|
|
this.diffOnly = checkbox.checked;
|
|
this.app.saveLocalStorage("ols-history-diffonly", this.diffOnly == true ? "1" : null);
|
|
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
|
});
|
|
});
|
|
diffOnlyLabel.appendText("Diff only");
|
|
diffOnlyLabel.addClass("diff-only-label");
|
|
diffOnlyLabel.style.display = this.showDiff ? "inline-block" : "none";
|
|
this.diffOnlyLabel = diffOnlyLabel;
|
|
|
|
// Diff navigation buttons
|
|
this.diffNavContainer = diffOptionsRow.createDiv("");
|
|
this.diffNavContainer.addClass("diff-nav");
|
|
this.diffNavContainer.style.display = this.showDiff ? "flex" : "none";
|
|
|
|
this.diffNavContainer.createEl("button", { text: "\u25B2 Prev" }, (e) => {
|
|
e.addClass("diff-nav-btn");
|
|
e.addEventListener("click", () => {
|
|
this.navigateDiff("prev");
|
|
});
|
|
});
|
|
this.diffNavContainer.createEl("button", { text: "\u25BC Next" }, (e) => {
|
|
e.addClass("diff-nav-btn");
|
|
e.addEventListener("click", () => {
|
|
this.navigateDiff("next");
|
|
});
|
|
});
|
|
this.diffNavIndicator = this.diffNavContainer.createEl("span", { text: "\u2014" });
|
|
this.diffNavIndicator.addClass("diff-nav-indicator");
|
|
|
|
this.info = contentEl.createDiv("");
|
|
this.info.addClass("op-info");
|
|
fireAndForget(async () => await this.loadFile(this.initialRev));
|
|
const div = contentEl.createDiv({ text: "Loading old revisions..." });
|
|
this.contentView = div;
|
|
div.addClass("op-scrollable");
|
|
div.addClass("op-pre");
|
|
const buttons = contentEl.createDiv("");
|
|
buttons.createEl("button", { text: "Copy to clipboard" }, (e) => {
|
|
e.addClass("mod-cta");
|
|
e.addEventListener("click", () => {
|
|
fireAndForget(async () => {
|
|
await navigator.clipboard.writeText(this.currentText);
|
|
Logger(`Old content copied to clipboard`, LOG_LEVEL_NOTICE);
|
|
});
|
|
});
|
|
});
|
|
const focusFile = async (path: string) => {
|
|
const targetFile = this.plugin.app.vault.getFileByPath(path);
|
|
if (targetFile) {
|
|
const leaf = this.plugin.app.workspace.getLeaf(false);
|
|
await leaf.openFile(targetFile);
|
|
} else {
|
|
Logger("Unable to display the file in the editor", LOG_LEVEL_NOTICE);
|
|
}
|
|
};
|
|
buttons.createEl("button", { text: "Back to this revision" }, (e) => {
|
|
e.addClass("mod-cta");
|
|
e.addEventListener("click", () => {
|
|
fireAndForget(async () => {
|
|
// const pathToWrite = this.plugin.id2path(this.id, true);
|
|
const pathToWrite = stripPrefix(this.file);
|
|
if (!isValidPath(pathToWrite)) {
|
|
Logger("Path is not valid to write content.", LOG_LEVEL_INFO);
|
|
return;
|
|
}
|
|
if (!this.currentDoc) {
|
|
Logger("No active file loaded.", LOG_LEVEL_INFO);
|
|
return;
|
|
}
|
|
const d = readContent(this.currentDoc);
|
|
await this.core.storageAccess.writeHiddenFileAuto(pathToWrite, d);
|
|
await focusFile(pathToWrite);
|
|
this.close();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
override onClose() {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
this.BlobURLs.forEach((value) => {
|
|
console.log(value);
|
|
if (value) URL.revokeObjectURL(value);
|
|
});
|
|
}
|
|
}
|