"use client"; import Link from "next/link"; import { CSSProperties, useEffect, useMemo, useRef, useState } from "react"; import { getBookEntries } from "@/lib/book-helpers"; import { Book } from "@/types/book"; type ReaderViewProps = { book: Book; chapterIndex: number; initialThemeKey?: string; initialFontKey?: string; initialWidthKey?: string; }; const themeOptions = [ { key: "warm", label: "暖米", page: "#f7f0e4", stage: "#d8d0c4", text: "#2b241e", catalog: "#e6ddd0" }, { key: "mist", label: "浅灰", page: "#efede7", stage: "#d6d4cf", text: "#2a2723", catalog: "#dfddd7" }, { key: "sepia", label: "茶褐", page: "#eee0cf", stage: "#d4c3b1", text: "#35281f", catalog: "#dfcdbb" } ] as const; const fontOptions = [ { key: "sm", label: "小", size: "1.02rem", lineHeight: 2.05 }, { key: "md", label: "中", size: "1.18rem", lineHeight: 2.28 }, { key: "lg", label: "大", size: "1.34rem", lineHeight: 2.55 } ] as const; const widthOptions = [ { key: "narrow", label: "窄", width: 760 }, { key: "medium", label: "中", width: 920 }, { key: "wide", label: "宽", width: 1080 } ] as const; export function ReaderView({ book, chapterIndex, initialThemeKey, initialFontKey, initialWidthKey }: ReaderViewProps) { const entries = useMemo(() => getBookEntries(book), [book]); const entry = entries[chapterIndex]; const prevChapterIndex = chapterIndex > 0 ? chapterIndex - 1 : null; const nextChapterIndex = chapterIndex < entries.length - 1 ? chapterIndex + 1 : null; const initialThemeIndex = Math.max(0, themeOptions.findIndex((item) => item.key === initialThemeKey)); const initialFontIndex = Math.max(0, fontOptions.findIndex((item) => item.key === initialFontKey)); const initialWidthIndex = Math.max(0, widthOptions.findIndex((item) => item.key === initialWidthKey)); const [themeIndex, setThemeIndex] = useState(initialThemeKey ? initialThemeIndex : 0); const [fontIndex, setFontIndex] = useState(initialFontKey ? initialFontIndex : 1); const [widthIndex, setWidthIndex] = useState(initialWidthKey ? initialWidthIndex : 1); const [catalogOpen, setCatalogOpen] = useState(false); const [progress, setProgress] = useState(0); const stageRef = useRef(null); useEffect(() => { const { page: pageColor } = themeOptions[themeIndex]; document.body.classList.add("reader-body"); const previousBodyBackground = document.body.style.background; const previousHtmlBackground = document.documentElement.style.background; document.body.style.background = pageColor; document.documentElement.style.background = pageColor; // Force iOS Safari to pick up theme-color by removing old tags and inserting a fresh one document.querySelectorAll('meta[name="theme-color"]').forEach((m) => m.remove()); const metaTheme = document.createElement("meta"); metaTheme.setAttribute("name", "theme-color"); metaTheme.setAttribute("content", pageColor); document.head.appendChild(metaTheme); return () => { document.body.classList.remove("reader-body"); document.body.style.background = previousBodyBackground; document.documentElement.style.background = previousHtmlBackground; metaTheme.setAttribute("content", "#f4efe7"); }; }, [themeIndex]); useEffect(() => { const stage = stageRef.current; if (!stage) return; stage.scrollTo({ top: 0, behavior: "auto" }); setCatalogOpen(false); const updateProgress = () => { const maxScroll = stage.scrollHeight - stage.clientHeight; const nextProgress = maxScroll <= 0 ? 0 : (stage.scrollTop / maxScroll) * 100; setProgress(nextProgress); }; updateProgress(); stage.addEventListener("scroll", updateProgress); window.addEventListener("resize", updateProgress); return () => { stage.removeEventListener("scroll", updateProgress); window.removeEventListener("resize", updateProgress); }; }, [chapterIndex]); useEffect(() => { const stage = stageRef.current; if (!stage) return; const maxScroll = stage.scrollHeight - stage.clientHeight; setProgress(maxScroll <= 0 ? 0 : (stage.scrollTop / maxScroll) * 100); }, [fontIndex, widthIndex, themeIndex]); const currentTheme = themeOptions[themeIndex]; const currentFont = fontOptions[fontIndex]; const currentWidth = widthOptions[widthIndex]; const entryWords = entry.content.join("").length; const isLoreEntry = entry.kind === "lore"; const shellWidthStyle = useMemo( () => ({ width: "var(--reader-shell-width)" }) as CSSProperties, [] ); const cycleFont = () => setFontIndex((value) => (value + 1) % fontOptions.length); const cycleTheme = () => setThemeIndex((value) => (value + 1) % themeOptions.length); const cycleWidth = () => setWidthIndex((value) => (value + 1) % widthOptions.length); const catalogContent = ( <>

目录

{book.sections .slice() .sort((a, b) => a.order - b.order) .map((section) => { const sectionEntries = entries.filter((item) => item.sectionId === section.id); return (
{section.kind === "lore" ? "资料" : "正文"} {section.title}
{sectionEntries.map((item, index) => ( setCatalogOpen(false)} > {section.kind === "lore" ? `资料 ${index + 1}` : `第 ${index + 1} 章`} {item.title} ))}
); })}
); return (
); }