import { type ReactNode, useEffect, useMemo, useRef, useState } from "react"; import { generateHTML } from "@tiptap/core"; import { COVER_ID, useBook } from "../store/useBook"; import { type Chapter, type TrimSize, bodyNumber, chapterKind, partNumber, partRoman } from "../model/book"; import { editorExtensions } from "../editor/extensions"; import { chapterToPdfInputs, coverToPdfInputs } from "../export/typst"; import { compilePdf, isDesktop } from "../ipc"; import { DEVICES, type Device, findDevice } from "../devices"; import { usePreviewMode } from "../store/usePreviewMode"; import { PdfPreview } from "./PdfPreview"; import { CoverArt } from "./CoverArt"; import { DeviceFrame } from "./DeviceFrame"; const TRIMS: { value: TrimSize; label: string }[] = [ { value: "6x9", label: "6 × 9 in" }, { value: "5.5x8.5", label: "5.5 × 8.5 in" }, { value: "5x8", label: "5 × 8 in" }, { value: "a5", label: "A5" }, ]; function PreviewSelect() { const trim = useBook((s) => s.book?.settings.trim ?? "6x9"); const setSettings = useBook((s) => s.setSettings); const mode = usePreviewMode((s) => s.mode); const setMode = usePreviewMode((s) => s.setMode); const value = mode === "print" ? trim : mode; return ( ); } function DockHead({ meta }: { meta?: ReactNode }) { return (
Preview
{meta}
); } function chapterEyebrow(chapter: Chapter, chapters: Chapter[], idx: number): string { const kind = chapterKind(chapter); if (kind === "body") return `Chapter ${bodyNumber(chapters, idx) ?? ""}`; if (kind === "part") return `Part ${partRoman(partNumber(chapters, idx) ?? 0)}`; return kind === "front" ? "Front matter" : "Back matter"; } function previewTitle(chapter: Chapter): string | undefined { if (chapter.noTitle) return undefined; if (chapterKind(chapter) === "part") return chapter.title || undefined; return chapter.title || "Untitled"; } function useActiveChapter() { const book = useBook((s) => s.book); const activeChapterId = useBook((s) => s.activeChapterId); const chapters = book?.chapters ?? []; const idx = chapters.findIndex((c) => c.id === activeChapterId); const chapter = chapters[idx] ?? chapters[0]; const html = useMemo(() => (chapter ? generateHTML(chapter.content, editorExtensions) : ""), [chapter]); const eyebrow = chapter ? chapterEyebrow(chapter, chapters, idx) : ""; return { book, coverActive: activeChapterId === COVER_ID, chapters, idx, chapter, html, eyebrow }; } export function Dock() { const mode = usePreviewMode((s) => s.mode); const device = findDevice(mode); if (device) return ; return isDesktop ? : ; } function PdfDock() { const book = useBook((s) => s.book); const activeChapterId = useBook((s) => s.activeChapterId); const [pdf, setPdf] = useState(null); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const timer = useRef>(undefined); useEffect(() => { if (!book) return; const coverActive = activeChapterId === COVER_ID; const idx = Math.max(0, book.chapters.findIndex((c) => c.id === activeChapterId)); clearTimeout(timer.current); setBusy(true); timer.current = setTimeout(() => { const { source, images, fonts } = coverActive ? coverToPdfInputs(book) : chapterToPdfInputs(book, idx); compilePdf(source, images, false, fonts) .then((bytes) => { setPdf(bytes); setError(null); }) .catch((e) => setError(String(e))) .finally(() => setBusy(false)); }, 350); return () => clearTimeout(timer.current); }, [book, activeChapterId]); return (
updating… : null} /> {error ? (
{error}
) : pdf ? ( ) : (
Typesetting…
)}
); } function HtmlDock() { const { book, coverActive, chapters, idx, chapter, html, eyebrow } = useActiveChapter(); if (!book) return null; if (coverActive) { return (
Cover} />
); } if (!chapter) return null; return (
{idx + 1} of {chapters.length} } />
{eyebrow}
{previewTitle(chapter) !== undefined &&
{previewTitle(chapter)}
}
{idx * 8 + 7}
); } function DeviceDock({ device }: { device: Device }) { const { book, coverActive, chapters, idx, chapter, html, eyebrow } = useActiveChapter(); if (!book) return null; if (coverActive) { return (
Cover} />
); } if (!chapter) { return (
No chapter selected.
); } return (
{idx + 1} of {chapters.length} } />
); }