mirror of
https://github.com/zadam/trilium.git
synced 2026-05-06 09:36:37 +02:00
feat(print): integrate with print preview and merge with export to PDF
This commit is contained in:
@@ -1808,7 +1808,7 @@
|
||||
"note_detail": {
|
||||
"could_not_find_typewidget": "Could not find typeWidget for type '{{type}}'",
|
||||
"printing": "Printing in progress...",
|
||||
"printing_pdf": "Exporting to PDF in progress...",
|
||||
"printing_pdf": "Preparing print preview...",
|
||||
"print_report_title": "Print report",
|
||||
"print_report_error_title": "Failed to print",
|
||||
"print_report_stack_trace": "Stack trace",
|
||||
@@ -2325,6 +2325,9 @@
|
||||
"title": "Print preview",
|
||||
"close": "Close",
|
||||
"save": "Save as PDF",
|
||||
"print": "Print",
|
||||
"export_pdf": "Export as PDF",
|
||||
"system_print": "Print using system dialog",
|
||||
"orientation": "Orientation",
|
||||
"portrait": "Portrait",
|
||||
"landscape": "Landscape",
|
||||
|
||||
@@ -182,57 +182,51 @@ export default function NoteDetail() {
|
||||
useTriliumEvent("printActiveNote", () => {
|
||||
if (!noteContext?.isActive() || !note) return;
|
||||
|
||||
showToast("printing");
|
||||
|
||||
if (isElectron()) {
|
||||
// On Electron, open the print preview dialog. Actual print/PDF actions
|
||||
// are triggered from the dialog's footer buttons.
|
||||
showToast("exporting_pdf");
|
||||
const { ipcRenderer } = dynamicRequire("electron");
|
||||
ipcRenderer.send("print-note", {
|
||||
notePath: noteContext.notePath
|
||||
ipcRenderer.send("export-as-pdf-preview", {
|
||||
title: note.title,
|
||||
notePath: noteContext.notePath,
|
||||
pageSize: note.getAttributeValue("label", "printPageSize") ?? "Letter",
|
||||
landscape: note.hasAttribute("label", "printLandscape"),
|
||||
scale: parseFloat(note.getAttributeValue("label", "printScale") ?? "1") || 1,
|
||||
margins: note.getAttributeValue("label", "printMargins") ?? "default",
|
||||
pageRanges: ""
|
||||
});
|
||||
} else {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = `?print#${noteContext.notePath}`;
|
||||
iframe.className = "print-iframe";
|
||||
document.body.appendChild(iframe);
|
||||
iframe.onload = () => {
|
||||
if (!iframe.contentWindow) {
|
||||
toast.closePersistent("printing");
|
||||
document.body.removeChild(iframe);
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
// Browser fallback: render the print page in a hidden iframe and use window.print().
|
||||
showToast("printing");
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = `?print#${noteContext.notePath}`;
|
||||
iframe.className = "print-iframe";
|
||||
document.body.appendChild(iframe);
|
||||
iframe.onload = () => {
|
||||
if (!iframe.contentWindow) {
|
||||
toast.closePersistent("printing");
|
||||
document.body.removeChild(iframe);
|
||||
return;
|
||||
}
|
||||
|
||||
iframe.contentWindow.addEventListener("note-load-progress", (e) => {
|
||||
showToast("printing", e.detail.progress);
|
||||
});
|
||||
|
||||
iframe.contentWindow.addEventListener("note-ready", (e) => {
|
||||
toast.closePersistent("printing");
|
||||
|
||||
if ("detail" in e) {
|
||||
handlePrintReport(e.detail as PrintReport);
|
||||
}
|
||||
|
||||
iframe.contentWindow.addEventListener("note-load-progress", (e) => {
|
||||
showToast("printing", e.detail.progress);
|
||||
});
|
||||
|
||||
iframe.contentWindow.addEventListener("note-ready", (e) => {
|
||||
toast.closePersistent("printing");
|
||||
|
||||
if ("detail" in e) {
|
||||
handlePrintReport(e.detail as PrintReport);
|
||||
}
|
||||
|
||||
iframe.contentWindow?.print();
|
||||
document.body.removeChild(iframe);
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
useTriliumEvent("exportAsPdf", () => {
|
||||
if (!noteContext?.isActive() || !note) return;
|
||||
showToast("exporting_pdf");
|
||||
|
||||
const { ipcRenderer } = dynamicRequire("electron");
|
||||
ipcRenderer.send("export-as-pdf-preview", {
|
||||
title: note.title,
|
||||
notePath: noteContext.notePath,
|
||||
pageSize: note.getAttributeValue("label", "printPageSize") ?? "Letter",
|
||||
landscape: note.hasAttribute("label", "printLandscape"),
|
||||
scale: parseFloat(note.getAttributeValue("label", "printScale") ?? "1") || 1,
|
||||
margins: note.getAttributeValue("label", "printMargins") ?? "default",
|
||||
pageRanges: ""
|
||||
});
|
||||
iframe.contentWindow?.print();
|
||||
document.body.removeChild(iframe);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -110,7 +110,7 @@ export default function PrintPreviewDialog() {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
function handleExportPdf() {
|
||||
if (!bufferRef.current) return;
|
||||
|
||||
const { ipcRenderer } = dynamicRequire("electron");
|
||||
@@ -121,6 +121,21 @@ export default function PrintPreviewDialog() {
|
||||
handleClose();
|
||||
}
|
||||
|
||||
function handlePrint(silent: boolean) {
|
||||
if (!isElectron()) return;
|
||||
const { ipcRenderer } = dynamicRequire("electron");
|
||||
ipcRenderer.send("print-from-preview", {
|
||||
notePath: notePathRef.current,
|
||||
pageSize,
|
||||
landscape,
|
||||
scale,
|
||||
margins: marginsStr,
|
||||
pageRanges,
|
||||
silent
|
||||
});
|
||||
handleClose();
|
||||
}
|
||||
|
||||
function handleOrientationChange(newLandscape: boolean) {
|
||||
if (newLandscape === landscape) return;
|
||||
setLandscape(newLandscape);
|
||||
@@ -215,10 +230,23 @@ export default function PrintPreviewDialog() {
|
||||
show={shown}
|
||||
onHidden={handleClose}
|
||||
bodyStyle={{ height: "78vh", padding: 0, display: "flex" }}
|
||||
footerAlignment="between"
|
||||
footer={
|
||||
<>
|
||||
<Button text={t("print_preview.close")} onClick={handleClose} />
|
||||
<Button text={t("print_preview.save")} className="btn-primary" onClick={handleSave} disabled={loading} />
|
||||
<a
|
||||
href="#"
|
||||
class={loading ? "disabled" : ""}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (!loading) handlePrint(false);
|
||||
}}
|
||||
>
|
||||
{t("print_preview.system_print")}
|
||||
</a>
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
<Button text={t("print_preview.export_pdf")} icon="bx-file" onClick={handleExportPdf} disabled={loading} />
|
||||
<Button text={t("print_preview.print")} icon="bx-printer" className="btn-primary" onClick={() => handlePrint(true)} disabled={loading} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -143,7 +143,6 @@ export function NoteContextMenu({ note, noteContext, itemsAtStart, itemsNearNote
|
||||
notePath: noteContext.notePath,
|
||||
defaultType: "single"
|
||||
})} />
|
||||
{isElectron && <CommandItem command="exportAsPdf" icon="bx bxs-file-pdf" disabled={!isPrintable} text={t("note_actions.print_pdf")} />}
|
||||
{isExportableToImage && isNormalViewMode && isContentAvailable && <ExportAsImage ntxId={noteContext.ntxId} parentComponent={parentComponent} />}
|
||||
<CommandItem command="printActiveNote" icon="bx bx-printer" disabled={!isPrintable} text={t("note_actions.print_note")} />
|
||||
|
||||
|
||||
@@ -270,6 +270,59 @@ electron.ipcMain.on("save-pdf", async (_e, { title, buffer }: { title: string; b
|
||||
electron.shell.openPath(filePath);
|
||||
});
|
||||
|
||||
interface PrintFromPreviewOpts extends ExportAsPdfOpts {
|
||||
silent: boolean;
|
||||
}
|
||||
|
||||
electron.ipcMain.on("print-from-preview", async (e, { notePath, landscape, pageSize, scale, margins, pageRanges, silent }: PrintFromPreviewOpts) => {
|
||||
try {
|
||||
const { browserWindow, printReport } = await getBrowserWindowForPrinting(e, notePath, "printing");
|
||||
|
||||
// print() accepts most of the same options as printToPDF, but typing differs
|
||||
// slightly (e.g. no "Ledger" pageSize). Cast to keep this concise.
|
||||
const printOpts: Electron.WebContentsPrintOptions = {
|
||||
silent,
|
||||
landscape,
|
||||
pageSize: pageSize === "Ledger" ? "Tabloid" : pageSize,
|
||||
scaleFactor: Math.round(scale * 100),
|
||||
margins: parseMargins(margins),
|
||||
pageRanges: parsePageRangesForPrint(pageRanges),
|
||||
printBackground: true
|
||||
};
|
||||
|
||||
browserWindow.webContents.print(printOpts, (success, failureReason) => {
|
||||
if (!success && failureReason !== "Print job canceled") {
|
||||
electron.dialog.showErrorBox(t("pdf.unable-to-print"), failureReason);
|
||||
}
|
||||
e.sender.send("print-from-preview-done");
|
||||
e.sender.send("print-done", printReport);
|
||||
browserWindow.destroy();
|
||||
});
|
||||
} catch (err) {
|
||||
e.sender.send("print-from-preview-done");
|
||||
e.sender.send("print-done", {
|
||||
type: "error",
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
stack: err instanceof Error ? err.stack : undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/** Convert "1-5, 8, 11-13" into PageRanges array form expected by webContents.print. */
|
||||
function parsePageRangesForPrint(pageRanges: string): { from: number; to: number }[] | undefined {
|
||||
if (!pageRanges?.trim()) return undefined;
|
||||
const ranges: { from: number; to: number }[] = [];
|
||||
for (const part of pageRanges.split(",")) {
|
||||
const trimmed = part.trim();
|
||||
if (!trimmed) continue;
|
||||
const [fromStr, toStr] = trimmed.split("-").map(s => s.trim());
|
||||
const from = parseInt(fromStr, 10);
|
||||
const to = toStr ? parseInt(toStr, 10) : from;
|
||||
if (!isNaN(from) && !isNaN(to)) ranges.push({ from, to });
|
||||
}
|
||||
return ranges.length ? ranges : undefined;
|
||||
}
|
||||
|
||||
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