diff --git a/app/src/history/doc.ts b/app/src/history/doc.ts
new file mode 100644
index 000000000..4f84a6fa8
--- /dev/null
+++ b/app/src/history/doc.ts
@@ -0,0 +1,187 @@
+import {Dialog} from "../dialog";
+import {confirmDialog} from "../dialog/confirmDialog";
+import {Constants} from "../constants";
+import {hasClosestByClassName} from "../protyle/util/hasClosest";
+import {renderAssetsPreview} from "../asset/renderAssets";
+import {Protyle} from "../protyle";
+import {disabledProtyle, onGet} from "../protyle/util/onGet";
+import * as dayjs from "dayjs";
+import {fetchPost} from "../util/fetch";
+import {isMobile} from "../util/functions";
+import {App} from "../index";
+
+let historyEditor: Protyle;
+
+const renderDoc = (element: HTMLElement, currentPage: number, id: string) => {
+ const previousElement = element.querySelector('[data-type="docprevious"]');
+ const nextElement = element.querySelector('[data-type="docnext"]');
+ element.setAttribute("data-page", currentPage.toString());
+ if (currentPage > 1) {
+ previousElement.removeAttribute("disabled");
+ } else {
+ previousElement.setAttribute("disabled", "disabled");
+ }
+ const opElement = element.querySelector('.b3-select[data-type="opselect"]') as HTMLSelectElement;
+ const docElement = element.querySelector('.history__text[data-type="docPanel"]');
+ const assetElement = element.querySelector('.history__text[data-type="assetPanel"]');
+ const mdElement = element.querySelector('.history__text[data-type="mdPanel"]') as HTMLTextAreaElement;
+ docElement.classList.add("fn__none");
+ mdElement.classList.add("fn__none");
+ assetElement.classList.remove("fn__none");
+ fetchPost("/api/history/searchHistory", {
+ query: id,
+ page: currentPage,
+ op: opElement.value,
+ type: 4
+ }, (response) => {
+ if (currentPage < response.data.pageCount) {
+ nextElement.removeAttribute("disabled");
+ } else {
+ nextElement.setAttribute("disabled", "disabled");
+ }
+ nextElement.nextElementSibling.nextElementSibling.textContent = `${currentPage}/${response.data.pageCount || 1}`;
+ if (response.data.histories.length === 0) {
+ element.lastElementChild.lastElementChild.previousElementSibling.classList.add("fn__none");
+ element.lastElementChild.lastElementChild.classList.add("fn__none");
+ element.lastElementChild.firstElementChild.innerHTML = `
${window.siyuan.languages.emptyContent}`;
+ return;
+ }
+ let logsHTML = "";
+ response.data.histories.forEach((item: string) => {
+ logsHTML += `
+
+ ${dayjs(parseInt(item) * 1000).format("YYYY-MM-DD HH:mm:ss")}
+`;
+ });
+ element.lastElementChild.firstElementChild.innerHTML = logsHTML;
+ });
+};
+
+export const openDocHistory = (app: App, id: string) => {
+ const contentHTML = `
+
+
+
+
+
+
1/1
+
+
+
+
+
+
+ - ${window.siyuan.languages.emptyContent}
+
+
+
+
+
+
`;
+ const dialog = new Dialog({
+ content: contentHTML,
+ width: isMobile() ? "92vw" : "768px",
+ height: isMobile() ? "80vh" : "70vh",
+ destroyCallback() {
+ historyEditor = undefined;
+ }
+ });
+ bindEvent(app, dialog.element, id);
+};
+
+const bindEvent = (app: App, element: HTMLElement, id: string) => {
+ element.querySelector(".b3-select").addEventListener("change", () => {
+ renderDoc(element, 1, id);
+ });
+ const docElement = element.querySelector('.history__text[data-type="docPanel"]') as HTMLElement;
+ const assetElement = element.querySelector('.history__text[data-type="assetPanel"]');
+ const mdElement = element.querySelector('.history__text[data-type="mdPanel"]') as HTMLTextAreaElement;
+ renderDoc(element, 1, id);
+ historyEditor = new Protyle(app, docElement, {
+ blockId: "",
+ action: [Constants.CB_GET_HISTORY],
+ render: {
+ background: false,
+ title: false,
+ gutter: false,
+ breadcrumb: false,
+ breadcrumbDocName: false,
+ },
+ typewriterMode: false,
+ });
+ disabledProtyle(historyEditor.protyle);
+ element.addEventListener("click", (event) => {
+ let target = event.target as HTMLElement;
+ while (target && !target.isEqualNode(element)) {
+ const type = target.getAttribute("data-type");
+ if (target.classList.contains("b3-list-item__action") && type === "rollback" && !window.siyuan.config.readonly) {
+ confirmDialog("⚠️ " + window.siyuan.languages.rollback, `${window.siyuan.languages.rollbackConfirm.replace("${date}", target.parentElement.textContent.trim())}`, () => {
+ const dataType = target.parentElement.getAttribute("data-type");
+ if (dataType === "assets") {
+ fetchPost("/api/history/rollbackAssetsHistory", {
+ historyPath: target.parentElement.getAttribute("data-path")
+ });
+ } else if (dataType === "doc") {
+ fetchPost("/api/history/rollbackDocHistory", {
+ // notebook: (firstPanelElement.querySelector('.b3-select[data-type="notebookselect"]') as HTMLSelectElement).value,
+ historyPath: target.parentElement.getAttribute("data-path")
+ });
+ }
+ });
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+ } else if (target.classList.contains("b3-list-item")) {
+ const dataPath = target.getAttribute("data-path");
+ if (type === "assets") {
+ assetElement.innerHTML = renderAssetsPreview(dataPath);
+ } else if (type === "doc") {
+ fetchPost("/api/history/getDocHistoryContent", {
+ historyPath: dataPath,
+ // k: (firstPanelElement.querySelector(".b3-text-field") as HTMLInputElement).value
+ }, (response) => {
+ if (response.data.isLargeDoc) {
+ mdElement.value = response.data.content;
+ mdElement.classList.remove("fn__none");
+ docElement.classList.add("fn__none");
+ } else {
+ mdElement.classList.add("fn__none");
+ docElement.classList.remove("fn__none");
+ onGet({
+ data: response,
+ protyle: historyEditor.protyle,
+ action: [Constants.CB_GET_HISTORY, Constants.CB_GET_HTML],
+ });
+ }
+ });
+ }
+ let currentItem = hasClosestByClassName(target, "b3-list") as HTMLElement;
+ if (currentItem) {
+ currentItem = currentItem.querySelector(".b3-list-item--focus");
+ if (currentItem) {
+ currentItem.classList.remove("b3-list-item--focus");
+ }
+ }
+ target.classList.add("b3-list-item--focus");
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+ } else if ((type === "docprevious" || type === "docnext") && target.getAttribute("disabled") !== "disabled") {
+ const currentPage = parseInt(element.getAttribute("data-page"));
+ renderDoc(element, type === "docprevious" ? currentPage - 1 : currentPage + 1, id);
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+ }
+ target = target.parentElement;
+ }
+ });
+};
diff --git a/app/src/menus/navigation.ts b/app/src/menus/navigation.ts
index 38910d0c3..fecc182c3 100644
--- a/app/src/menus/navigation.ts
+++ b/app/src/menus/navigation.ts
@@ -32,6 +32,7 @@ import {openNewWindowById} from "../window/openNewWindow";
import {openCardByData} from "../card/openCard";
import {viewCards} from "../card/viewCards";
import {App} from "../index";
+import {openDocHistory} from "../history/doc";
const initMultiMenu = (selectItemElements: NodeListOf) => {
const fileItemElement = Array.from(selectItemElements).find(item => {
@@ -540,6 +541,15 @@ export const initFileMenu = (app: App, notebookId: string, pathString: string, l
submenu: openSubmenus,
}).element);
/// #endif
+ if (!window.siyuan.config.readonly) {
+ window.siyuan.menus.menu.append(new MenuItem({
+ label: window.siyuan.languages.dataHistory,
+ icon: "iconHistory",
+ click() {
+ openDocHistory(app, id);
+ }
+ }).element);
+ }
genImportMenu(notebookId, pathString);
window.siyuan.menus.menu.append(exportMd(id));
return window.siyuan.menus.menu;