import { createEffect, createResource, createSignal, For, onCleanup, onMount, Show, } from "solid-js"; import { Portal } from "solid-js/web"; import { TbOutlineDownload, TbOutlinePrinter, TbOutlineRefresh, TbOutlineX, } from "solid-icons/tb"; import { getIndexedData } from "../../data-loader/file-index"; import { extractSection } from "../../data-loader"; import { parseMarkdown } from "../../markdown"; import type { PrintConfig } from "./types"; import { columnWidthMm, createPageElement, mmToPx, paginate, waitForStableSize, type PaginateOptions, } from "./paginate"; import { exportPrintPDF } from "./print-export"; function formatTimestamp(date: Date): string { const pad = (n: number) => String(n).padStart(2, "0"); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`; } export interface PrintPreviewProps { src: string; section?: string; articlePath: string; config: PrintConfig; onClose: () => void; } async function fetchPrintContent(params: { src: string; section?: string; }): Promise { const text = await getIndexedData(params.src); return params.section ? extractSection(text, params.section) : text; } export function PrintPreview(props: PrintPreviewProps) { const [content] = createResource( () => ({ src: props.src, section: props.section }), fetchPrintContent, ); const [pages, setPages] = createSignal([]); const [paginating, setPaginating] = createSignal(false); const [warnings, setWarnings] = createSignal([]); const [error, setError] = createSignal(null); const [scale, setScale] = createSignal(0.8); const [sourceEl, setSourceEl] = createSignal(); const [mountEl, setMountEl] = createSignal(); const [sheetEl, setSheetEl] = createSignal(); let running = false; const title = () => props.config.title || props.src.split("/").pop() || "打印预览"; const options = (): PaginateOptions => ({ ...props.config, articlePath: props.articlePath, }); const applyFooter = (pageList: HTMLElement[], timestamp: string) => { pageList.forEach((page, index) => { const footer = page.querySelector(".md-print-footer"); if (!footer) return; const pageLabel = props.config.pageNumbers ? `${index + 1} / ${pageList.length}` : ""; footer.textContent = [pageLabel, timestamp].filter(Boolean).join(" · "); // Left A5 gets the left corner, right A5 gets the right corner. footer.classList.add( index % 2 === 0 ? "md-print-footer--left" : "md-print-footer--right", ); }); }; const runPagination = async () => { const md = content(); const source = sourceEl(); const mount = mountEl(); if (md === undefined || !source || !mount || running) return; running = true; setPaginating(true); setError(null); setWarnings([]); setPages([]); mount.innerHTML = ""; try { source.innerHTML = parseMarkdown(md, props.articlePath); await document.fonts.ready; await waitForStableSize(source, 300, 4000); const result = paginate(source, options(), mount); applyFooter( result.pages, props.config.timestamp ? formatTimestamp(new Date()) : "", ); setPages(result.pages); setWarnings(result.warnings); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { running = false; setPaginating(false); } }; createEffect(() => { if (content() !== undefined && sourceEl() && mountEl()) { void runPagination(); } }); createEffect(() => { const container = sheetEl(); const pageList = pages(); if (!container) return; container.innerHTML = ""; for (let i = 0; i < pageList.length; i += 2) { const sheet = document.createElement("div"); sheet.className = "md-print-sheet md-print-sheet--preview"; sheet.appendChild(pageList[i]); if (pageList[i + 1]) { sheet.appendChild(pageList[i + 1]); } else { sheet.appendChild(createPageElement(options()).page); } if (props.config.cutMarks) { const line = document.createElement("div"); line.className = "md-print-cut-line"; sheet.appendChild(line); } const wrapper = document.createElement("div"); wrapper.className = "md-print-sheet-wrapper"; wrapper.appendChild(sheet); container.appendChild(wrapper); } }); onMount(() => { const container = sheetEl(); if (container) { const observer = new ResizeObserver(() => { const available = container.clientWidth; if (available <= 0) return; setScale(Math.min(1, available / mmToPx(297))); }); observer.observe(container); onCleanup(() => observer.disconnect()); } const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") props.onClose(); }; document.addEventListener("keydown", onKeyDown); onCleanup(() => document.removeEventListener("keydown", onKeyDown)); }); const handleExport = async () => { try { // Refresh the timestamp so the exported file records the export time. if (props.config.timestamp) { applyFooter(pages(), formatTimestamp(new Date())); } await exportPrintPDF(pages(), props.config, title(), props.articlePath); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } }; return (
{title()}
{pages().length} 页 · A4 横版 · 每页 {props.config.columns} 列
正在分页…
{error() || content.error?.message}
0}>
{(warning) =>
· {warning}
}
{/* Hidden measuring container: one column wide, same typography. */}
{/* Hidden mount for pages while they are being measured. */}
); }