From 093c9d3804cc95d42b61c659510018d19f492502 Mon Sep 17 00:00:00 2001 From: 9527La <9527La@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:19:18 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=A0=87=E7=AD=BE=E9=A1=B5?= =?UTF-8?q?=E5=8F=B3=E9=94=AE=E8=8F=9C=E5=8D=95=E5=A2=9E=E5=BC=BA=20+=20PD?= =?UTF-8?q?F=20=E5=AF=BC=E5=87=BA=E6=A0=B7=E5=BC=8F=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=EF=BC=88=E5=AD=97=E4=BD=93/=E9=A2=9C=E8=89=B2/24=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF/=E5=AE=9E=E6=97=B6=E9=A2=84=E8=A7=88=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/pdf_export.rs | 1 - src/App.vue | 67 +++ src/components/SettingsDialog.vue | 912 +++++++++++++++++++++++------- src/components/TabBar.vue | 98 +++- src/composables/exportStyles.ts | 75 ++- src/composables/useExport.ts | 38 +- src/composables/usePdfPreview.ts | 130 +++++ src/composables/usePdfStyle.ts | 746 ++++++++++++++++++++++++ src/i18n/en-US.ts | 75 ++- src/i18n/zh-CN.ts | 74 ++- 10 files changed, 1970 insertions(+), 246 deletions(-) create mode 100644 src/composables/usePdfPreview.ts create mode 100644 src/composables/usePdfStyle.ts diff --git a/src-tauri/src/pdf_export.rs b/src-tauri/src/pdf_export.rs index 2601508..ebf22b0 100644 --- a/src-tauri/src/pdf_export.rs +++ b/src-tauri/src/pdf_export.rs @@ -88,7 +88,6 @@ fn run_edge_print( cmd.arg("--no-first-run"); cmd.arg("--no-default-browser-check"); cmd.arg("--no-pdf-header-footer"); - cmd.arg("--no-margins"); cmd.arg("--run-all-compositor-stages-before-draw"); cmd.arg(format!( "--virtual-time-budget={}", diff --git a/src/App.vue b/src/App.vue index 1057e04..ef8b228 100644 --- a/src/App.vue +++ b/src/App.vue @@ -173,6 +173,27 @@ async function openInExplorer(path: string) { errorMsg.value = e?.message ?? String(e); } } + +async function copyPath(path: string) { + if (!path) return; + try { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(path); + } else { + const ta = document.createElement("textarea"); + ta.value = path; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + document.execCommand("copy"); + document.body.removeChild(ta); + } + exportToast.value = t("tabs.copiedPath"); + } catch { + exportToast.value = t("tabs.copyFailed"); + } +} const dialogFileName = computed(() => dialogTab.value ? basename(dialogTab.value.path) : "" ); @@ -412,6 +433,48 @@ function onDialogCancel() { resolveDialog("cancel"); } +async function closeOthers(id: string) { + const keep = tabs.value.find((x) => x.id === id); + if (!keep || tabs.value.length <= 1) return; + const others = tabs.value.filter((x) => x.id !== id); + for (const tab of others) { + if (tab.isDirty) { + if (tab.id !== activeTabId.value) switchToTab(tab.id); + const choice = await askUnsaved(tab, "unsaved"); + if (choice === "cancel") return; + if (choice === "save") { + const ok = await saveTab(tab); + if (!ok) return; + } + } + } + for (const tab of others) removeTab(tab.id); + activateTab(keep.id); +} + +async function closeAll() { + const ok = await confirmCloseAll(); + if (!ok) return; + for (const tab of [...tabs.value]) removeTab(tab.id); +} + +async function refreshTab(id: string) { + const tab = tabs.value.find((x) => x.id === id); + if (!tab) return; + if (tab.isDirty) { + if (id !== activeTabId.value) switchToTab(id); + const choice = await askUnsaved(tab, "external"); + if (choice === "cancel") return; + if (choice === "save") { + // "重新加载" — discard unsaved edits and reload from disk. + await forceReloadTab(tab); + } + // "保留编辑" — keep current edits, do not reload. + return; + } + await forceReloadTab(tab); +} + function getPreviewTopSourceLine(): number { const container = viewerEl.value; const body = bodyRef.value; @@ -1338,7 +1401,11 @@ watch( :active-tab-id="activeTabId" @activate="switchToTab" @close="closeTab" + @close-others="closeOthers" + @close-all="closeAll" + @refresh="refreshTab" @reveal-file="openInExplorer" + @copy-path="copyPath" />
diff --git a/src/components/SettingsDialog.vue b/src/components/SettingsDialog.vue index e8c6095..4719d67 100644 --- a/src/components/SettingsDialog.vue +++ b/src/components/SettingsDialog.vue @@ -6,6 +6,8 @@ import { open } from "@tauri-apps/plugin-dialog"; import { openUrl } from "@tauri-apps/plugin-opener"; import { useI18n } from "vue-i18n"; import { useReadingSettings } from "../composables/useReadingSettings"; +import { usePdfStyle } from "../composables/usePdfStyle"; +import { usePdfPreview } from "../composables/usePdfPreview"; import { getCachedPandocRefDoc, setCachedPandocRefDoc, @@ -152,6 +154,7 @@ const props = defineProps<{ visible: boolean }>(); const emit = defineEmits<{ (e: "close"): void }>(); const showShortcuts = ref(false); +const settingsTab = ref<"reading" | "pdf">("reading"); watch( () => props.visible, (v) => { @@ -172,6 +175,29 @@ const { reset, } = useReadingSettings(); +const { + settings: pdfStyle, + templates: pdfTemplates, + templateCategories: pdfTemplateCategories, + fontChoices: pdfFontChoices, + codeFontChoices: pdfCodeFontChoices, + applyTemplate: applyPdfTemplate, + setOption: setPdfOption, + setDensity: setPdfDensity, + reset: resetPdfStyle, +} = usePdfStyle(); + +const pdfTemplateMap = computed>( + () => { + const m: Record = {}; + for (const tpl of pdfTemplates) m[tpl.id] = tpl; + return m; + } +); + +const { previewHtml, previewLight, previewDark, sampleText, compareMode } = + usePdfPreview(); + interface SystemFont { name: string; monospaced: boolean; @@ -231,227 +257,584 @@ async function registerAssociations() { -
- - - {{ settings.fontSize }}px +
+ +
-
- - - {{ settings.editorFontSize }}px -
+
+
+ + + {{ settings.fontSize }}px +
-
- - -
+
+ + + {{ settings.editorFontSize }}px +
-
- - - {{ settings.lineHeight.toFixed(2) }} -
+
+ + +
-
- - - {{ settings.maxWidth }}px -
+
+ + + {{ settings.lineHeight.toFixed(2) }} +
-
- - + {{ settings.maxWidth }}px +
+ +
+ + -
+ + + + +
-
- - -
+
+ + +
-
-
-
{{ t("settings.updateCheck") }}
-
- {{ - t("settings.currentVersion", { version: currentVersion || "-" }) - }} +
+
+
{{ t("settings.updateCheck") }}
+
+ {{ + t("settings.currentVersion", { version: currentVersion || "-" }) + }} +
+
+ {{ updateMessage }} +
-
- {{ updateMessage }} +
+ + +
+
+ +
+
+
+ {{ t("settings.pandocTemplate") }} +
+
+ + {{ pandocRefDoc }} + + {{ t("settings.pandocTemplateHint") }} +
+
+
+ +
-
- -
+ +
+
+
{{ t("shortcuts.title") }}
+
{{ t("shortcuts.hint") }}
+
+
-
-
-
-
- {{ t("settings.pandocTemplate") }} +
+ +
+ +
+
{{ t("pdfStyle.title") }}
+
{{ t("pdfStyle.previewNote") }}
+ +
+ {{ t("pdfStyle.preview") }} +
-
- + + + +
+ +
-
-
- - -
-
-
-
-
- {{ t("settings.fileAssociation") }} +
+ +
-
- {{ t("settings.fileAssociationHint") }} + +
+ +
-
- {{ associationMessage }} + +
+ + +
+ +
+ + + {{ pdfStyle.fontSize }}px +
+ +
+ + + {{ pdfStyle.lineHeight.toFixed(2) }} +
+ +
+ +
+ + + +
+
+ +
+ + + {{ pdfStyle.textColor }} +
+ +
+ + + {{ pdfStyle.headingColor }}
-
- -
-
-
-
{{ t("shortcuts.title") }}
-
{{ t("shortcuts.hint") }}
+
+ + + {{ pdfStyle.linkColor }} +
+ +
+ + + {{ pdfStyle.codeBg }} +
+ +
+ + + {{ pdfStyle.bgColor }} +
+ +
+ + + +
+ +
+ + + {{ pdfStyle.pageMargin }}mm +
+ +
+ +
- -
-
+
- @@ -90,7 +101,9 @@ export async function exportToHtml( filters: [{ name: "HTML", extensions: ["html", "htm"] }], }); if (!dest) return null; - const html = await buildExportHtml(body, baseName); + const html = await buildExportHtml(body, baseName, { + pdfStyle: usePdfStyle().settings.value, + }); await writeTextFile(dest, html); return dest; } @@ -132,7 +145,10 @@ async function pandocExport( filters: [{ name: prettyName, extensions: [ext] }], }); if (!dest) return null; - const html = await buildExportHtml(body, title, { forceLight: true }); + const html = await buildExportHtml(body, title, { + forceLight: true, + pdfStyle: getDefaultPdfStyle(), + }); return await invoke("export_with_pandoc", { opts: { html, @@ -212,7 +228,9 @@ export async function exportToPdf( filters: [{ name: "PDF", extensions: ["pdf"] }], }); if (!dest) return null; - const html = await buildExportHtml(body, title, { forceLight: true }); + const html = await buildExportHtml(body, title, { + pdfStyle: usePdfStyle().settings.value, + }); const cached = getCachedEdgePath(); try { return await callEdge(html, dest, cached); @@ -229,7 +247,7 @@ export async function exportToPdf( } export function printDocument(body: HTMLElement, title: string) { - buildExportHtml(body, title, { forceLight: true }) + buildExportHtml(body, title, { pdfStyle: usePdfStyle().settings.value }) .then((html) => { const blob = new globalThis.Blob([html], { type: "text/html" }); const url = globalThis.URL.createObjectURL(blob); diff --git a/src/composables/usePdfPreview.ts b/src/composables/usePdfPreview.ts new file mode 100644 index 0000000..eea9214 --- /dev/null +++ b/src/composables/usePdfPreview.ts @@ -0,0 +1,130 @@ +// Live preview for the PDF export style panel. Reuses the exact same pipeline as +// the real export (renderMarkdown + buildExportHtml) so what you see is what you get, +// rendered in isolated