From d0e61e39d0bbd2d244b71b05841478106707bcd9 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 15 Apr 2026 18:17:28 +0300 Subject: [PATCH] feat(print): selectable printer --- .../src/widgets/dialogs/print_preview.tsx | 76 +++++++++++++++++-- apps/server/src/services/window.ts | 12 ++- 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/apps/client/src/widgets/dialogs/print_preview.tsx b/apps/client/src/widgets/dialogs/print_preview.tsx index 4512707eee..1ee73ae777 100644 --- a/apps/client/src/widgets/dialogs/print_preview.tsx +++ b/apps/client/src/widgets/dialogs/print_preview.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef, useState } from "preact/hooks"; +import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"; import FNote from "../../entities/fnote"; import { t } from "../../services/i18n"; @@ -13,6 +13,15 @@ import OptionsRow from "../type_widgets/options/components/OptionsRow"; import OptionsSection from "../type_widgets/options/components/OptionsSection"; const PAGE_SIZES = ["A0", "A1", "A2", "A3", "A4", "A5", "A6", "Legal", "Letter", "Tabloid", "Ledger"] as const; + +/** Pseudo-printer name used to route the Print button to the PDF export flow. */ +const DESTINATION_PDF = "__pdf__"; + +interface PrinterInfo { + name: string; + displayName: string; + isDefault: boolean; +} const MARGIN_PRESETS = ["default", "none", "minimum"] as const; type MarginPreset = typeof MARGIN_PRESETS[number]; @@ -81,6 +90,21 @@ export default function PrintPreviewDialog() { const [pageRanges, setPageRanges] = useState(""); const pageRangesValid = isValidPageRanges(pageRanges); + // Printer list and current destination. DESTINATION_PDF means "Save as PDF"; + // any other value is the system printer name to use for silent printing. + const [printers, setPrinters] = useState([]); + const [destination, setDestination] = useState(DESTINATION_PDF); + + useEffect(() => { + if (!shown || !isElectron()) return; + const { ipcRenderer } = dynamicRequire("electron"); + ipcRenderer.invoke("get-printers").then((list: PrinterInfo[]) => { + setPrinters(list ?? []); + const defaultPrinter = list?.find((p) => p.isDefault); + if (defaultPrinter) setDestination(defaultPrinter.name); + }); + }, [shown]); + const updatePreview = useCallback((buffer: Uint8Array) => { bufferRef.current = buffer; @@ -121,7 +145,7 @@ export default function PrintPreviewDialog() { handleClose(); } - function handlePrint(silent: boolean) { + function handlePrint(silent: boolean, deviceName?: string) { if (!isElectron()) return; const { ipcRenderer } = dynamicRequire("electron"); ipcRenderer.send("print-from-preview", { @@ -131,11 +155,21 @@ export default function PrintPreviewDialog() { scale, margins: marginsStr, pageRanges, - silent + silent, + deviceName }); handleClose(); } + /** Primary action: route to PDF export or silent print based on the selected destination. */ + function handlePrimaryAction() { + if (destination === DESTINATION_PDF) { + handleExportPdf(); + } else { + handlePrint(true, destination); + } + } + function handleOrientationChange(newLandscape: boolean) { if (newLandscape === landscape) return; setLandscape(newLandscape); @@ -238,20 +272,46 @@ export default function PrintPreviewDialog() { class={loading ? "disabled" : ""} onClick={(e) => { e.preventDefault(); - if (!loading) handlePrint(false); + if (loading) return; + // When a specific printer is selected, pre-select it in the system dialog. + const deviceName = destination === DESTINATION_PDF ? undefined : destination; + handlePrint(false, deviceName); }} > {t("print_preview.system_print")} -
-
+