diff --git a/src/apps/webapp/bootstrap.ts b/src/apps/webapp/bootstrap.ts index 2450285..b3fa072 100644 --- a/src/apps/webapp/bootstrap.ts +++ b/src/apps/webapp/bootstrap.ts @@ -41,7 +41,7 @@ async function renderHistoryList(): Promise { const [items, lastUsedId] = await Promise.all([historyStore.getVaultHistory(), historyStore.getLastUsedVaultId()]); - listEl.innerHTML = ""; + listEl.replaceChildren(); emptyEl.classList.toggle("is-hidden", items.length > 0); for (const item of items) { diff --git a/src/lib b/src/lib index 6a2dc67..6c53e74 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 6a2dc6777f1eb2beb7a058b8d2dde662662df9d7 +Subproject commit 6c53e748eb3dff92514e1cd28359007c8fcb3173 diff --git a/src/modules/features/DocumentHistory/DocumentHistoryModal.ts b/src/modules/features/DocumentHistory/DocumentHistoryModal.ts index 7e7560a..40a5254 100644 --- a/src/modules/features/DocumentHistory/DocumentHistoryModal.ts +++ b/src/modules/features/DocumentHistory/DocumentHistoryModal.ts @@ -1,6 +1,6 @@ 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, escapeStringToHTML, readString } from "../../../lib/src/string_and_binary/convert.ts"; +import { decodeBinary, readString } from "../../../lib/src/string_and_binary/convert.ts"; import ObsidianLiveSyncPlugin from "../../../main.ts"; import { type DocumentID, @@ -145,22 +145,66 @@ export class DocumentHistoryModal extends Modal { return v; } + prepareContentView(usePreformatted = true) { + this.contentView.empty(); + this.contentView.toggleClass("op-pre", usePreformatted); + } + + appendTextDiff(diff: [number, string][]) { + for (const [operation, text] of diff) { + if (operation == DIFF_DELETE) { + this.contentView.createSpan({ text, cls: "history-deleted" }); + } else if (operation == DIFF_EQUAL) { + this.contentView.createSpan({ text, cls: "history-normal" }); + } else if (operation == DIFF_INSERT) { + this.contentView.createSpan({ text, cls: "history-added" }); + } + } + } + + 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.innerHTML = ""; - this.contentView.innerHTML = `Could not read this revision
(${rev})`; + 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.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`; - let result = undefined; + this.info.setText(`Modified:${new Date(w.mtime).toLocaleString()}`); const w1data = readDocument(w); this.currentDeleted = !!w.deleted; - // this.currentText = w1data; + 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) { @@ -168,58 +212,55 @@ export class DocumentHistoryModal extends Modal { const w2 = await db.getDBEntry(this.file, { rev: oldRev }, false, false, true); if (w2 != false) { if (typeof w1data == "string") { - result = ""; - const dmp = new diff_match_patch(); - const w2data = readDocument(w2) as string; - const diff = dmp.diff_main(w2data, w1data); - 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) + ""; + 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; } - result = result.replace(/\n/g, "
"); } else if (isImage(this.file)) { const src = this.generateBlobURL("base", w1data); const overlay = this.generateBlobURL( "overlay", readDocument(w2) as Uint8Array ); - result = `
-
- - -
-
`; - this.contentView.removeClass("op-pre"); + this.prepareContentView(false); + if (this.currentDeleted) { + this.appendDeletedNotice(false); + } + this.appendImageDiff(src, overlay); + rendered = true; } } } } - if (result == undefined) { + if (!rendered) { if (typeof w1data != "string") { if (isImage(this.file)) { const src = this.generateBlobURL("base", w1data); - result = `
-
- -
-
`; - this.contentView.removeClass("op-pre"); + this.prepareContentView(false); + if (this.currentDeleted) { + this.appendDeletedNotice(false); + } + this.appendImageDiff(src); + } else { + if (this.currentDeleted) { + this.appendDeletedNotice(); + } + this.contentView.appendText("Binary file"); } } else { - result = escapeStringToHTML(w1data); + if (this.currentDeleted) { + this.appendDeletedNotice(); + } + this.contentView.appendText(w1data); } } - if (result == undefined) result = typeof w1data == "string" ? escapeStringToHTML(w1data) : "Binary file"; - this.contentView.innerHTML = - (this.currentDeleted ? "(At this revision, the file has been deleted)\n" : "") + result; } // Reset diff navigation after content changes this.resetDiffNavigation(); diff --git a/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts b/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts index ad308e5..eec5332 100644 --- a/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts +++ b/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts @@ -1,7 +1,6 @@ import { App, Modal } from "../../../deps.ts"; import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch"; import { CANCELLED, LEAVE_TO_SUBSEQUENT, type diff_result } from "../../../lib/src/common/types.ts"; -import { escapeStringToHTML } from "../../../lib/src/string_and_binary/convert.ts"; import { delay } from "../../../lib/src/common/utils.ts"; import { eventHub } from "../../../common/events.ts"; import { globalSlipBoard } from "../../../lib/src/bureau/bureau.ts"; @@ -44,6 +43,25 @@ export class ConflictResolveModal extends Modal { // sendValue("close-resolve-conflict:" + this.filename, false); } + appendDiffFragment(container: HTMLDivElement, text: string, cls: string) { + const lines = text.split("\n"); + lines.forEach((line, index) => { + const span = container.createSpan({ cls }); + span.textContent = line; + if (index < lines.length - 1) { + container.createSpan({ cls: "ls-mark-cr" }); + container.createEl("br"); + } + }); + } + + appendVersionInfo(container: HTMLDivElement, cls: string, name: string, date: string) { + const line = container.createSpan({ cls }); + line.createSpan({ text: name, cls: "conflict-dev-name" }); + line.appendText(`: ${date}`); + container.createEl("br"); + } + override onOpen() { const { contentEl } = this; // Send cancel signal for the previous merge dialogue @@ -64,25 +82,21 @@ export class ConflictResolveModal extends Modal { const div = contentEl.createDiv(""); div.addClass("op-scrollable"); div.addClass("ls-dialog"); - let diff = ""; + let diffLength = 0; for (const v of this.result.diff) { const x1 = v[0]; const x2 = v[1]; + diffLength += x2.length; + if (diffLength > 100 * 1024) { + continue; + } if (x1 == DIFF_DELETE) { - diff += - "" + - escapeStringToHTML(x2).replace(/\n/g, "\n") + - ""; + this.appendDiffFragment(div, x2, "deleted"); + div.createEl("span", { text: x2, cls: "deleted normal conflict-dev-name" }); } else if (x1 == DIFF_EQUAL) { - diff += - "" + - escapeStringToHTML(x2).replace(/\n/g, "\n") + - ""; + this.appendDiffFragment(div, x2, "normal"); } else if (x1 == DIFF_INSERT) { - diff += - "" + - escapeStringToHTML(x2).replace(/\n/g, "\n") + - ""; + this.appendDiffFragment(div, x2, "added"); } } @@ -92,8 +106,8 @@ export class ConflictResolveModal extends Modal { new Date(this.result.left.mtime).toLocaleString() + (this.result.left.deleted ? " (Deleted)" : ""); const date2 = new Date(this.result.right.mtime).toLocaleString() + (this.result.right.deleted ? " (Deleted)" : ""); - div2.innerHTML = `${this.localName}: ${date1}
-${this.remoteName}: ${date2}
`; + this.appendVersionInfo(div2, "deleted", this.localName, date1); + this.appendVersionInfo(div2, "added", this.remoteName, date2); contentEl.createEl("button", { text: `Use ${this.localName}` }, (e) => e.addEventListener("click", () => this.sendResponse(this.result.right.rev)) ).style.marginRight = "4px"; @@ -108,11 +122,9 @@ export class ConflictResolveModal extends Modal { contentEl.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) => e.addEventListener("click", () => this.sendResponse(CANCELLED)) ).style.marginRight = "4px"; - diff = diff.replace(/\n/g, "
"); - if (diff.length > 100 * 1024) { + if (diffLength > 100 * 1024) { + div.empty(); div.innerText = "(Too large diff to display)"; - } else { - div.innerHTML = diff; } } diff --git a/src/modules/features/SettingDialogue/PaneChangeLog.ts b/src/modules/features/SettingDialogue/PaneChangeLog.ts index be0e0c3..3d8ceb9 100644 --- a/src/modules/features/SettingDialogue/PaneChangeLog.ts +++ b/src/modules/features/SettingDialogue/PaneChangeLog.ts @@ -43,10 +43,13 @@ export function paneChangeLog(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElem // tmpDiv.addClass("sls-header-button"); tmpDiv.addClass("op-warn-info"); - tmpDiv.innerHTML = `

${$msg("obsidianLiveSyncSettingTab.msgNewVersionNote")}

`; + tmpDiv.createEl("p", { text: $msg("obsidianLiveSyncSettingTab.msgNewVersionNote") }); + const readEverythingButton = tmpDiv.createEl("button", { + text: $msg("obsidianLiveSyncSettingTab.optionOkReadEverything"), + }); if (lastVersion > (this.editingSettings?.lastReadUpdates || 0)) { const informationButtonDiv = informationDivEl.appendChild(tmpDiv); - informationButtonDiv.querySelector("button")?.addEventListener("click", () => { + readEverythingButton.addEventListener("click", () => { fireAndForget(async () => { this.editingSettings.lastReadUpdates = lastVersion; await this.saveAllDirtySettings(); diff --git a/src/modules/features/SettingDialogue/PaneSetup.ts b/src/modules/features/SettingDialogue/PaneSetup.ts index de996bc..40cd88b 100644 --- a/src/modules/features/SettingDialogue/PaneSetup.ts +++ b/src/modules/features/SettingDialogue/PaneSetup.ts @@ -125,8 +125,13 @@ export function paneSetup( paneEl, "div", "", - (el) => - (el.innerHTML = `${$msg("obsidianLiveSyncSettingTab.linkOpenInBrowser")}`) + (el) => { + el.createEl("a", { text: $msg("obsidianLiveSyncSettingTab.linkOpenInBrowser") }, (anchor) => { + anchor.href = `https://github.com/${repo}/blob/main${topPath}`; + anchor.target = "_blank"; + anchor.rel = "noopener"; + }); + } ); const troubleShootEl = this.createEl(paneEl, "div", { text: "", diff --git a/src/modules/features/SettingDialogue/utilFixCouchDBSetting.ts b/src/modules/features/SettingDialogue/utilFixCouchDBSetting.ts index 041c923..d061643 100644 --- a/src/modules/features/SettingDialogue/utilFixCouchDBSetting.ts +++ b/src/modules/features/SettingDialogue/utilFixCouchDBSetting.ts @@ -13,7 +13,7 @@ export const checkConfig = async ( Logger($msg("obsidianLiveSyncSettingTab.logCheckingDbConfig"), LOG_LEVEL_INFO); let isSuccessful = true; const emptyDiv = createDiv(); - emptyDiv.innerHTML = ""; + emptyDiv.createSpan(); checkResultDiv?.replaceChildren(...[emptyDiv]); const addResult = (msg: string, classes?: string[]) => { const tmpDiv = createDiv(); @@ -21,7 +21,7 @@ export const checkConfig = async ( if (classes) { tmpDiv.addClasses(classes); } - tmpDiv.innerHTML = `${msg}`; + tmpDiv.textContent = msg; checkResultDiv?.appendChild(tmpDiv); }; try { @@ -47,9 +47,10 @@ export const checkConfig = async ( if (!checkResultDiv) return; const tmpDiv = createDiv(); tmpDiv.addClass("ob-btn-config-fix"); - tmpDiv.innerHTML = ``; + tmpDiv.createEl("label", { text: title }); + const fixButton = tmpDiv.createEl("button", { text: $msg("obsidianLiveSyncSettingTab.btnFix") }); const x = checkResultDiv.appendChild(tmpDiv); - x.querySelector("button")?.addEventListener("click", () => { + fixButton.addEventListener("click", () => { fireAndForget(async () => { Logger($msg("obsidianLiveSyncSettingTab.logCouchDbConfigSet", { title, key, value })); const res = await requestToCouchDBWithCredentials(