From 8831a9b277d57dd13fa14df3831bedd9e141e5c6 Mon Sep 17 00:00:00 2001 From: XingfenD Date: Wed, 16 Sep 2026 17:41:35 +0800 Subject: [PATCH] feat(frontend): EPUB typography themes + chrome migration (B3) --- docs/CHANGELOG_web.md | 3 + frontend/src/components/reader-settings.tsx | 20 +-- frontend/src/readers/EpubReader.tsx | 153 ++++++++++++++++---- 3 files changed, 141 insertions(+), 35 deletions(-) diff --git a/docs/CHANGELOG_web.md b/docs/CHANGELOG_web.md index 50e439f..0679daf 100644 --- a/docs/CHANGELOG_web.md +++ b/docs/CHANGELOG_web.md @@ -10,6 +10,9 @@ The format loosely follows Keep a Changelog and can be adapted to the team's hab ### Added / 新增 +- EPUB reader gains typography settings (font size, line height, margins) and honors the 纸/米/夜 reading themes including follow-global-theme; its toolbar moves to the unified settings bar. +- EPUB 阅读器新增排版设置(字号/行距/边距)并支持纸/米/夜阅读主题(含跟随全局);工具条迁入统一设置栏。 + - Text reader gains full-book search (搜索 tab in the navigation drawer: debounced input, chapter + snippet results, tap to jump) and typography settings (line height ×5, margin width ×3); the reading toolbar is now the unified settings bar with theme swatches and follow-global-theme. - 文本阅读器新增全书搜索(导航抽屉「搜索」选项卡:输入防抖、章节+前后文结果、点击跳转)与排版设置(行距五档、边距三档);阅读工具条换为统一设置栏,含主题卡与跟随全局主题。 diff --git a/frontend/src/components/reader-settings.tsx b/frontend/src/components/reader-settings.tsx index 7f1777d..6741f05 100644 --- a/frontend/src/components/reader-settings.tsx +++ b/frontend/src/components/reader-settings.tsx @@ -6,8 +6,8 @@ import { useGlobalDark } from "@/components/theme"; import { RD_THEMES, RD_THEME_LABEL, FONT_SIZES, resolveRdTheme, useReaderPrefs, type RdTheme } from "@/lib/readerPrefs"; export interface ReaderSettingsProps { - /** 进度滑条:value/max 页或千分比由调用方定,onChange 收整数 */ - slider: { value: number; max: number; onChange: (v: number) => void; ariaLabel: string }; + /** 进度滑条:value/max 页或千分比由调用方定,onChange 收整数;EPUB 无全局分数故可省 */ + slider?: { value: number; max: number; onChange: (v: number) => void; ariaLabel: string }; /** 位置文本,如 "3/12 · 25%" */ position?: string; /** 左半区按钮(导航/上一章/下一章等),调用方给 */ @@ -105,13 +105,15 @@ export function ReaderSettings({ slider, position, left, showFont = false, onFon )} - slider.onChange(v)} - aria-label={slider.ariaLabel} - /> + {slider && ( + slider.onChange(v)} + aria-label={slider.ariaLabel} + /> + )} {extra && open &&
{extra}
} ); diff --git a/frontend/src/readers/EpubReader.tsx b/frontend/src/readers/EpubReader.tsx index 122b504..18de5c3 100644 --- a/frontend/src/readers/EpubReader.tsx +++ b/frontend/src/readers/EpubReader.tsx @@ -3,8 +3,11 @@ import { useEffect, useRef, useState } from "react"; import { apiRaw } from "../api/client"; import { BookmarksTab } from "../components/Bookmarks"; import { ReaderNav } from "../components/reader-nav"; -import { btn } from "../components/ui"; +import { ReaderSettings, SettingRow } from "../components/reader-settings"; +import { Button } from "@/components/ui/button"; import { toast } from "../components/Toaster"; +import { useGlobalDark } from "../components/theme"; +import { FONT_SIZES, LINE_HEIGHTS, MARGINS, MARGIN_LABEL, resolveRdTheme, useReaderPrefs } from "../lib/readerPrefs"; import { useProgressSaver, type ReaderProps } from "../lib/useProgress"; // 仅声明用到的最小子集,避免依赖 epubjs 自带类型的完整性 @@ -16,6 +19,10 @@ interface EpubRendition { next(): unknown; prev(): unknown; on(ev: "relocated", cb: (loc: EpubLocation) => void): void; + themes: { + register(name: string, rules: Record): void; + select(name: string): void; + }; destroy(): void; } interface EpubBookLike { @@ -30,10 +37,20 @@ async function loadEpub(): Promise<(data: Uint8Array) => EpubBookLike> { return m.default as (data: Uint8Array) => EpubBookLike; } -export default function EpubReader({ book, initialLocator, initialPercent }: ReaderProps) { +/** epubjs iframe 取不到宿主 CSS 变量 → 三主题 bg/fg 用 index.css 的具体色值常量。 */ +const THEME_CSS = { + paper: { bg: "#faf8f4", fg: "#292524" }, + sepia: { bg: "#f0e6d2", fg: "#433a29" }, + night: { bg: "#171512", fg: "#c6c0b6" }, +} as const; + +export default function EpubReader({ book, initialLocator, initialPercent }: Omit) { const hostRef = useRef(null); const rendRef = useRef(null); const saver = useProgressSaver(book.id, { locator: initialLocator, percent: initialPercent }); + const pr = useReaderPrefs(); + const globalDark = useGlobalDark(); + const rd = resolveRdTheme(pr.prefs.themeMode, globalDark); const [nav, setNav] = useState(false); const [err, setErr] = useState(""); const [nonce, setNonce] = useState(0); @@ -89,42 +106,74 @@ export default function EpubReader({ book, initialLocator, initialPercent }: Rea }; }, [book.file_url, nonce]); // eslint-disable-line react-hooks/exhaustive-deps + // 排版/主题映射:rd 或 prefs.epub 变化时重新 register+select(iframe 内须用具体色值) + useEffect(() => { + const r = rendRef.current; + if (!r || !ready) return; + const { bg, fg } = THEME_CSS[rd]; + r.themes.register("booklib", { + body: { + background: bg, + color: fg, + "line-height": String(LINE_HEIGHTS[pr.prefs.epub.lineIdx]), + "font-size": `${FONT_SIZES[pr.prefs.epub.sizeIdx]}px`, + padding: `${[0.5, 1, 1.75][pr.prefs.epub.marginIdx]}rem 0`, + }, + }); + r.themes.select("booklib"); + }, [rd, pr.prefs.epub, ready]); + if (err) return ( -
+
-

{err}

- +
); return ( -
-
-
+
+
+
{!ready && !err && ( -
EPUB 加载中…
+
EPUB 加载中…
)}
-
- - - -
+ + + + + + } + showFont + onFont={pr.bumpEpub} + extra={ + <> + + + + + } + /> {nav && ( ); } + +/** 字号档位按钮组(EPUB 独立档位,存 prefs.epub.sizeIdx)。 */ +function FontSizeRow({ pr }: { pr: ReturnType }) { + return ( + + {FONT_SIZES.map((fs, i) => ( + + ))} + + ); +} + +/** EPUB 行距/边距档位(存 prefs.epub)。 */ +function LineHeightRow({ pr }: { pr: ReturnType }) { + return ( + + {LINE_HEIGHTS.map((lh, i) => ( + + ))} + + ); +} +function MarginRow({ pr }: { pr: ReturnType }) { + return ( + + {MARGINS.map((m, i) => ( + + ))} + + ); +}