mirror of
https://github.com/zadam/trilium.git
synced 2026-05-06 10:46:57 +02:00
feat(print): basic PDF preview
This commit is contained in:
@@ -24,6 +24,7 @@ import { IncludeNoteOpts } from "../widgets/dialogs/include_note.jsx";
|
||||
import type { InfoProps } from "../widgets/dialogs/info.jsx";
|
||||
import type { MarkdownImportOpts } from "../widgets/dialogs/markdown_import.jsx";
|
||||
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
||||
import type { PrintPreviewData } from "../widgets/dialogs/print_preview.jsx";
|
||||
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||
import type NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import Component from "./component.js";
|
||||
@@ -330,6 +331,7 @@ export type CommandMappings = {
|
||||
toggleRightPane: CommandData;
|
||||
printActiveNote: CommandData;
|
||||
exportAsPdf: CommandData;
|
||||
showPrintPreview: PrintPreviewData;
|
||||
openNoteExternally: CommandData;
|
||||
openNoteCustom: CommandData;
|
||||
openNoteOnServer: CommandData;
|
||||
|
||||
@@ -24,6 +24,7 @@ import InfoDialog from "../widgets/dialogs/info.js";
|
||||
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
|
||||
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
|
||||
import PopupEditorDialog from "../widgets/dialogs/PopupEditor.jsx";
|
||||
import PrintPreviewDialog from "../widgets/dialogs/print_preview.jsx";
|
||||
import ToastContainer from "../widgets/Toast.jsx";
|
||||
|
||||
export function applyModals(rootContainer: RootContainer) {
|
||||
@@ -51,6 +52,7 @@ export function applyModals(rootContainer: RootContainer) {
|
||||
.child(<PromptDialog />)
|
||||
.child(<IncorrectCpuArchDialog />)
|
||||
.child(<PopupEditorDialog />)
|
||||
.child(<PrintPreviewDialog />)
|
||||
.child(<CallToActionDialog />)
|
||||
.child(<ToastContainer />);
|
||||
}
|
||||
|
||||
@@ -2305,6 +2305,11 @@
|
||||
"toggle": "Toggle right panel",
|
||||
"custom_widget_go_to_source": "Go to source code"
|
||||
},
|
||||
"print_preview": {
|
||||
"title": "Print preview",
|
||||
"close": "Close",
|
||||
"save": "Save as PDF"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_one": "{{count}} attachment",
|
||||
"attachments_other": "{{count}} attachments",
|
||||
|
||||
@@ -4,6 +4,7 @@ import clsx from "clsx";
|
||||
import { isValidElement, VNode } from "preact";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext from "../components/app_context";
|
||||
import NoteContext from "../components/note_context";
|
||||
import FNote from "../entities/fnote";
|
||||
import type { PrintReport } from "../print";
|
||||
@@ -146,11 +147,17 @@ export default function NoteDetail() {
|
||||
toast.closePersistent("printing");
|
||||
handlePrintReport(printReport);
|
||||
};
|
||||
const onPreviewResult = (_e: any, { buffer, title }: { buffer: Uint8Array; title: string }) => {
|
||||
toast.closePersistent("printing");
|
||||
appContext.triggerCommand("showPrintPreview", { pdfBuffer: buffer, title });
|
||||
};
|
||||
ipcRenderer.on("print-progress", onPrintProgress);
|
||||
ipcRenderer.on("print-done", onPrintDone);
|
||||
ipcRenderer.on("export-as-pdf-preview-result", onPreviewResult);
|
||||
return () => {
|
||||
ipcRenderer.off("print-progress", onPrintProgress);
|
||||
ipcRenderer.off("print-done", onPrintDone);
|
||||
ipcRenderer.off("export-as-pdf-preview-result", onPreviewResult);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -215,7 +222,7 @@ export default function NoteDetail() {
|
||||
showToast("exporting_pdf");
|
||||
|
||||
const { ipcRenderer } = dynamicRequire("electron");
|
||||
ipcRenderer.send("export-as-pdf", {
|
||||
ipcRenderer.send("export-as-pdf-preview", {
|
||||
title: note.title,
|
||||
notePath: noteContext.notePath,
|
||||
pageSize: note.getAttributeValue("label", "printPageSize") ?? "Letter",
|
||||
|
||||
68
apps/client/src/widgets/dialogs/print_preview.tsx
Normal file
68
apps/client/src/widgets/dialogs/print_preview.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useRef, useState } from "preact/hooks";
|
||||
import Modal from "../react/Modal";
|
||||
import PdfViewer from "../type_widgets/file/PdfViewer";
|
||||
import Button from "../react/Button";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import { t } from "../../services/i18n";
|
||||
import { dynamicRequire } from "../../services/utils";
|
||||
|
||||
export interface PrintPreviewData {
|
||||
pdfBuffer: Uint8Array;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default function PrintPreviewDialog() {
|
||||
const [shown, setShown] = useState(false);
|
||||
const [pdfUrl, setPdfUrl] = useState<string>();
|
||||
const bufferRef = useRef<Uint8Array>();
|
||||
const titleRef = useRef("");
|
||||
|
||||
useTriliumEvent("showPrintPreview", (data: PrintPreviewData) => {
|
||||
bufferRef.current = data.pdfBuffer;
|
||||
titleRef.current = data.title;
|
||||
|
||||
const blob = new Blob([data.pdfBuffer as BlobPart], { type: "application/pdf" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
setPdfUrl(url);
|
||||
setShown(true);
|
||||
});
|
||||
|
||||
function handleClose() {
|
||||
setShown(false);
|
||||
if (pdfUrl) {
|
||||
URL.revokeObjectURL(pdfUrl);
|
||||
setPdfUrl(undefined);
|
||||
}
|
||||
bufferRef.current = undefined;
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
if (!bufferRef.current) return;
|
||||
|
||||
const { ipcRenderer } = dynamicRequire("electron");
|
||||
ipcRenderer.send("save-pdf", {
|
||||
title: titleRef.current,
|
||||
buffer: bufferRef.current
|
||||
});
|
||||
handleClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="print-preview-dialog"
|
||||
title={t("print_preview.title")}
|
||||
size="xl"
|
||||
show={shown}
|
||||
onHidden={handleClose}
|
||||
bodyStyle={{ height: "80vh", padding: 0 }}
|
||||
footer={
|
||||
<>
|
||||
<Button text={t("print_preview.close")} onClick={handleClose} />
|
||||
<Button text={t("print_preview.save")} className="btn-primary" onClick={handleSave} />
|
||||
</>
|
||||
}
|
||||
>
|
||||
{pdfUrl && <PdfViewer pdfUrl={pdfUrl} />}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -168,6 +168,66 @@ electron.ipcMain.on("export-as-pdf", async (e, { title, notePath, landscape, pag
|
||||
}
|
||||
});
|
||||
|
||||
electron.ipcMain.on("export-as-pdf-preview", async (e, { title, notePath, landscape, pageSize }: ExportAsPdfOpts) => {
|
||||
try {
|
||||
const { browserWindow, printReport } = await getBrowserWindowForPrinting(e, notePath, "exporting_pdf");
|
||||
|
||||
try {
|
||||
const buffer = await browserWindow.webContents.printToPDF({
|
||||
landscape,
|
||||
pageSize,
|
||||
generateDocumentOutline: true,
|
||||
generateTaggedPDF: true,
|
||||
printBackground: true,
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: `<div></div>`,
|
||||
footerTemplate: `
|
||||
<div class="pageNumber" style="width: 100%; text-align: center; font-size: 10pt;">
|
||||
</div>
|
||||
`
|
||||
});
|
||||
|
||||
e.sender.send("export-as-pdf-preview-result", { buffer, title });
|
||||
} catch (_e) {
|
||||
electron.dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-export-message"));
|
||||
} finally {
|
||||
e.sender.send("print-done", printReport);
|
||||
browserWindow.destroy();
|
||||
}
|
||||
} catch (err) {
|
||||
e.sender.send("print-done", {
|
||||
type: "error",
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
stack: err instanceof Error ? err.stack : undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
electron.ipcMain.on("save-pdf", async (_e, { title, buffer }: { title: string; buffer: Buffer }) => {
|
||||
const focusedWindow = electron.BrowserWindow.getFocusedWindow();
|
||||
if (!focusedWindow) return;
|
||||
|
||||
const filePath = electron.dialog.showSaveDialogSync(focusedWindow, {
|
||||
defaultPath: formatDownloadTitle(title, "file", "application/pdf"),
|
||||
filters: [
|
||||
{
|
||||
name: t("pdf.export_filter"),
|
||||
extensions: ["pdf"]
|
||||
}
|
||||
]
|
||||
});
|
||||
if (!filePath) return;
|
||||
|
||||
try {
|
||||
await fs.writeFile(filePath, Buffer.from(buffer));
|
||||
} catch (_e) {
|
||||
electron.dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-save-message"));
|
||||
return;
|
||||
}
|
||||
|
||||
electron.shell.openPath(filePath);
|
||||
});
|
||||
|
||||
async function getBrowserWindowForPrinting(e: IpcMainEvent, notePath: string, action: "printing" | "exporting_pdf") {
|
||||
// Offscreen rendering crashes on Wayland due to a Chromium bug where the OSR surface
|
||||
// lacks a valid xdg_toplevel role, causing a fatal zxdg_exporter_v2 protocol error.
|
||||
|
||||
Reference in New Issue
Block a user