Files
book-comic-library/frontend/src/components/reader-settings.tsx
T

117 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ChevronDown, ChevronUp } from "lucide-react";
import { useState, type ReactNode } from "react";
import { Button } from "@/components/ui/button";
import { Slider } from "@/components/ui/slider";
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 };
/** 位置文本,如 "3/12 · 25%" */
position?: string;
/** 左半区按钮(导航/上一章/下一章等),调用方给 */
left?: ReactNode;
/** 是否显示 A−/A+ 与字号(text/epub true;cbz/pdf false) */
showFont?: boolean;
/** 「更多设置」展开区内容(按格式给:CBZ 翻页/RTL/连读/预读;Text 行距/边距;EPUB 字号/行距/边距) */
extra?: ReactNode;
}
/** 主题色卡样式(纸 #faf8f4 / 米 #f0e6d2 / 夜 #171512,与 --rd-bg 三值一致) */
const SWATCH: Record<RdTheme, string> = { paper: "#faf8f4", sepia: "#f0e6d2", night: "#171512" };
/** 统一阅读设置栏:常驻行 = 进度滑条 + 位置 + 左区 + 主题卡(含跟随全局)+ A−/A+;「更多设置」展开区按格式给。 */
export function ReaderSettings({ slider, position, left, showFont = false, extra }: ReaderSettingsProps) {
const pr = useReaderPrefs();
const globalDark = useGlobalDark();
const rd = resolveRdTheme(pr.prefs.themeMode, globalDark);
const [open, setOpen] = useState(false);
return (
<div
className="border-t bg-background/95 text-foreground backdrop-blur pt-1.5 pb-[max(0.5rem,env(safe-area-inset-bottom))]"
data-rd={rd}
>
<div className="flex items-center gap-2.5 px-2.5">
{left && <div className="flex items-center gap-1.5">{left}</div>}
{position && (
<span className="shrink-0 text-xs tabular-nums text-muted-foreground" aria-live="polite">
{position}
</span>
)}
<div className="flex items-center gap-1.5 whitespace-nowrap" role="group" aria-label="阅读主题">
{RD_THEMES.map((t) => (
<button
key={t}
aria-label={`${RD_THEME_LABEL[t]}色主题`}
aria-pressed={pr.prefs.themeMode === t}
onClick={() => pr.setThemeMode(t)}
className="h-5 w-5 shrink-0 rounded-full transition-transform hover:scale-110"
style={{
background: SWATCH[t],
boxShadow:
rd === t
? "0 0 0 2px var(--rd-accent)"
: "inset 0 0 0 1px color-mix(in oklab, currentColor 35%, transparent)",
}}
/>
))}
<Button
variant="ghost"
size="xs"
aria-pressed={pr.prefs.themeMode === "auto"}
className="text-muted-foreground"
onClick={() => pr.setThemeMode("auto")}
>
跟随全局
</Button>
</div>
<div className="ml-auto flex shrink-0 items-center gap-1">
{showFont && (
<>
<Button
variant="ghost"
size="icon-xs"
aria-label="减小字号"
disabled={pr.prefs.text.sizeIdx === 0}
onClick={() => pr.bumpText(-1)}
>
A−
</Button>
<Button
variant="ghost"
size="icon-xs"
aria-label="加大字号"
disabled={pr.prefs.text.sizeIdx === FONT_SIZES.length - 1}
onClick={() => pr.bumpText(1)}
>
A+
</Button>
</>
)}
{extra && (
<Button
variant="ghost"
size="icon-xs"
aria-label="更多阅读设置"
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
>
{open ? <ChevronUp className="size-4" /> : <ChevronDown className="size-4" />}
</Button>
)}
</div>
</div>
<Slider
className="mx-2.5 mt-2 w-[calc(100%-1.25rem)]"
value={[slider.value]}
max={slider.max}
onValueChange={([v]) => slider.onChange(v)}
aria-label={slider.ariaLabel}
/>
{extra && open && <div className="px-2.5 pt-2">{extra}</div>}
</div>
);
}