diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bbdc993e..31be2529 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,6 +18,7 @@ "js-base64": "^3.7.7", "jwt-decode": "^4.0.0", "lodash-es": "^4.17.21", + "marked": "^14.1.0", "material-icons": "^1.13.12", "normalize.css": "^8.0.1", "pinia": "^2.1.7", @@ -5810,6 +5811,18 @@ "node": ">=12" } }, + "node_modules/marked": { + "version": "14.1.0", + "resolved": "https://registry.npmmirror.com/marked/-/marked-14.1.0.tgz", + "integrity": "sha512-P93GikH/Pde0hM5TAXEd8I4JAYi8IB03n8qzW8Bh1BIEFpEyBoYxi/XWZA53LSpTeLBiMQOoSMj0u5E/tiVYTA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/marks-pane": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/marks-pane/-/marks-pane-1.0.9.tgz", diff --git a/frontend/package.json b/frontend/package.json index aaf7ecf4..a46c6e38 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,6 +29,7 @@ "jwt-decode": "^4.0.0", "lodash-es": "^4.17.21", "material-icons": "^1.13.12", + "marked": "^14.1.0", "normalize.css": "^8.0.1", "pinia": "^2.1.7", "pretty-bytes": "^6.1.1", diff --git a/frontend/src/css/mdPreview.css b/frontend/src/css/mdPreview.css new file mode 100644 index 00000000..bf2863f6 --- /dev/null +++ b/frontend/src/css/mdPreview.css @@ -0,0 +1,13 @@ +.md_preview { + overflow-y: auto; + max-height: 80vh; + padding: 1rem; + border: 1px solid #000; + font-size: 20px; + line-height: 1.2; +} + +#preview-container { + overflow: auto; + max-height: 80vh; /* Match the max-height of md_preview for scrolling */ +} diff --git a/frontend/src/css/styles.css b/frontend/src/css/styles.css index 4b57328d..19b94b95 100644 --- a/frontend/src/css/styles.css +++ b/frontend/src/css/styles.css @@ -16,6 +16,7 @@ @import "./login.css"; @import "./mobile.css"; @import "./epubReader.css"; +@import "./mdPreview.css"; /* For testing only :focus { diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 00d43b9d..1360bbec 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -24,6 +24,7 @@ "ok": "OK", "permalink": "Get Permanent Link", "previous": "Previous", + "preview": "Preview", "publish": "Publish", "rename": "Rename", "replace": "Replace", diff --git a/frontend/src/i18n/zh-cn.json b/frontend/src/i18n/zh-cn.json index 1aea4134..376dc029 100644 --- a/frontend/src/i18n/zh-cn.json +++ b/frontend/src/i18n/zh-cn.json @@ -22,6 +22,7 @@ "ok": "确定", "permalink": "获取永久链接", "previous": "上一个", + "preview": "预览", "publish": "发布", "rename": "重命名", "replace": "替换", diff --git a/frontend/src/views/files/Editor.vue b/frontend/src/views/files/Editor.vue index 214d8d4f..7e4d7a9b 100644 --- a/frontend/src/views/files/Editor.vue +++ b/frontend/src/views/files/Editor.vue @@ -11,11 +11,26 @@ :label="t('buttons.save')" @action="save()" /> + + -
+ +
+ +
@@ -33,10 +48,11 @@ import Breadcrumbs from "@/components/Breadcrumbs.vue"; import { useAuthStore } from "@/stores/auth"; import { useFileStore } from "@/stores/file"; import { useLayoutStore } from "@/stores/layout"; -import { inject, onBeforeUnmount, onMounted, ref } from "vue"; +import { inject, onBeforeUnmount, onMounted, ref, watchEffect } from "vue"; import { useRoute, useRouter } from "vue-router"; import { useI18n } from "vue-i18n"; import { getTheme } from "@/utils/theme"; +import { marked } from "marked"; const $showError = inject("$showError")!; @@ -51,11 +67,37 @@ const router = useRouter(); const editor = ref(null); +const isPreview = ref(false); +const previewContent = ref(""); +const isMarkdownFile = + fileStore.req?.name.endsWith(".md") || + fileStore.req?.name.endsWith(".markdown"); + onMounted(() => { window.addEventListener("keydown", keyEvent); + window.addEventListener("wheel", handleScroll); const fileContent = fileStore.req?.content || ""; + watchEffect(async () => { + if (isMarkdownFile && isPreview.value) { + const new_value = editor.value?.getValue() || ""; + try { + previewContent.value = await marked(new_value); + } catch (error) { + console.error("Failed to convert content to HTML:", error); + previewContent.value = ""; + } + + const previewContainer = document.getElementById("preview-container"); + if (previewContainer) { + previewContainer.addEventListener("wheel", handleScroll, { + capture: true, + }); + } + } + }); + ace.config.set( "basePath", `https://cdn.jsdelivr.net/npm/ace-builds@${ace_version}/src-min-noconflict/` @@ -82,6 +124,7 @@ onMounted(() => { onBeforeUnmount(() => { window.removeEventListener("keydown", keyEvent); + window.removeEventListener("wheel", handleScroll); editor.value?.destroy(); }); @@ -102,6 +145,13 @@ const keyEvent = (event: KeyboardEvent) => { save(); }; +const handleScroll = (event: WheelEvent) => { + const editorContainer = document.getElementById("preview-container"); + if (editorContainer) { + editorContainer.scrollTop += event.deltaY; + } +}; + const save = async () => { const button = "save"; buttons.loading("save"); @@ -126,4 +176,8 @@ const close = () => { let uri = url.removeLastDir(route.path) + "/"; router.push({ path: uri }); }; + +const preview = () => { + isPreview.value = !isPreview.value; +};