feat(reader): stop detecting 卷 in txt splitting (volume lines stay body text, volume-only books fall back to pseudo-sections); replace chapter dropdown with a 目录 sidebar overlay (active row highlighted + centered, click-away/pick to close)
This commit is contained in:
@@ -131,7 +131,9 @@ function TxtView({ book, initialLocator }: ReaderProps) {
|
||||
const inited = useRef(false);
|
||||
const restoreFrac = useRef<number | null>(null);
|
||||
const targetCi = useRef(0);
|
||||
const activeRow = useRef<HTMLButtonElement>(null);
|
||||
const [ci, setCi] = useState(0);
|
||||
const [toc, setToc] = useState(false);
|
||||
const pr = useReaderPrefs();
|
||||
const { text, err, retry } = useFileText(book.file_url ?? "");
|
||||
|
||||
@@ -174,6 +176,10 @@ function TxtView({ book, initialLocator }: ReaderProps) {
|
||||
saver.report({ ch: i, scrollFraction: 0 }, i / chapters.length);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (toc) activeRow.current?.scrollIntoView({ block: "center" });
|
||||
}, [toc]);
|
||||
|
||||
function onScroll() {
|
||||
const el = boxRef.current;
|
||||
if (!el || !chapters) return;
|
||||
@@ -185,44 +191,14 @@ function TxtView({ book, initialLocator }: ReaderProps) {
|
||||
if (err) return <LoadState text={err} onRetry={retry} />;
|
||||
if (text == null || !chapters) return <LoadState text="正文加载中…" />;
|
||||
|
||||
const chapterOptions = (() => {
|
||||
const groups: { vol: string | undefined; items: number[] }[] = [];
|
||||
chapters.forEach((c, i) => {
|
||||
const last = groups[groups.length - 1];
|
||||
if (!last || last.vol !== c.vol) groups.push({ vol: c.vol, items: [i] });
|
||||
else last.items.push(i);
|
||||
});
|
||||
const opt = (i: number) => (
|
||||
<option key={i} value={i}>
|
||||
{i + 1}. {chapters[i].title}
|
||||
</option>
|
||||
);
|
||||
return groups.length === 1 && !groups[0].vol
|
||||
? chapters.map((_, i) => opt(i))
|
||||
: groups.map((g, gi) =>
|
||||
g.vol ? (
|
||||
<optgroup key={gi} label={g.vol}>
|
||||
{g.items.map(opt)}
|
||||
</optgroup>
|
||||
) : (
|
||||
g.items.map(opt)
|
||||
),
|
||||
);
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="rd-surface flex h-full flex-col" data-rd={pr.prefs.theme}>
|
||||
<div className="rd-surface relative flex h-full flex-col" data-rd={pr.prefs.theme}>
|
||||
<ReaderToolbar pr={pr}>
|
||||
{chapters.length > 1 && (
|
||||
<>
|
||||
<select
|
||||
className="rd-select max-w-72 truncate"
|
||||
value={ci}
|
||||
aria-label="选择章节"
|
||||
onChange={(e) => goChapter(Number(e.target.value))}
|
||||
>
|
||||
{chapterOptions}
|
||||
</select>
|
||||
<button className="rd-btn" aria-expanded={toc} aria-controls="toc-panel" onClick={() => setToc((v) => !v)}>
|
||||
目录
|
||||
</button>
|
||||
<span className="text-xs opacity-60 tabular-nums">
|
||||
{ci + 1}/{chapters.length}
|
||||
</span>
|
||||
@@ -247,6 +223,34 @@ function TxtView({ book, initialLocator }: ReaderProps) {
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* ponytail: 目录一次渲染全部章节,数千章级书若卡顿再上虚拟列表 */}
|
||||
{toc && chapters.length > 1 && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-10" aria-hidden="true" onClick={() => setToc(false)} />
|
||||
<nav
|
||||
id="toc-panel"
|
||||
aria-label="章节目录"
|
||||
className="rd-divider absolute inset-y-0 left-0 z-20 w-64 overflow-y-auto border-r bg-[var(--rd-bg)] p-2 shadow-2xl"
|
||||
>
|
||||
<ul className="space-y-0.5">
|
||||
{chapters.map((c, i) => (
|
||||
<li key={i}>
|
||||
<button
|
||||
ref={i === ci ? activeRow : undefined}
|
||||
className={"rd-row" + (i === ci ? " rd-btn-on" : "")}
|
||||
onClick={() => {
|
||||
goChapter(i);
|
||||
setToc(false);
|
||||
}}
|
||||
>
|
||||
{i + 1}. {c.title}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user