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 (
);
}
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 (
);
}
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 (
);
}
if (!chapter) {
return (
);
}
return (
{idx + 1} of {chapters.length}
}
/>
);
}