feat(web): unify reader TOC and bookmarks into one left nav drawer with tabs
This commit is contained in:
@@ -2,7 +2,8 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useMemo, useRef, useState, type MouseEvent } from "react";
|
||||
import { api, formatPageUrl } from "../api/client";
|
||||
import { btn } from "../components/ui";
|
||||
import { useBookmarks } from "../components/Bookmarks";
|
||||
import { BookmarksTab } from "../components/Bookmarks";
|
||||
import { ReaderNav, type NavTab } from "../components/reader-nav";
|
||||
import { fetchObjectUrl } from "../lib/authImage";
|
||||
import { useProgressSaver, type ReaderProps } from "../lib/useProgress";
|
||||
import { PageHeights, chapterIndexAt, chapterWin } from "../lib/virt";
|
||||
@@ -79,10 +80,9 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
|
||||
});
|
||||
const count = countQ.data?.count ?? 0;
|
||||
const chapters = countQ.data?.chapters ?? null; // 包内目录结构(第N話/上中下卷);null/单组=扁平整本一章
|
||||
const [toc, setToc] = useState(false);
|
||||
const [nav, setNav] = useState<null | NavTab>(null);
|
||||
const activeRow = useRef<HTMLButtonElement>(null);
|
||||
const saver = useProgressSaver(book.id, { locator: initialLocator, percent: initialPercent });
|
||||
const bm = useBookmarks({ book, capture: saver.capture, seek: (l) => goPage(Number(l.page) || 0) });
|
||||
const boxRef = useRef<HTMLDivElement>(null);
|
||||
const [width, setWidth] = useState(480);
|
||||
const [vh, setVh] = useState(600);
|
||||
@@ -190,8 +190,8 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
|
||||
const cur = locked ? Math.min(curRaw, w.end - 1) : curRaw; // 锁章:视口落在章末卡片上时读数不越章(HUD/滑条/进度)
|
||||
|
||||
useEffect(() => {
|
||||
if (toc) activeRow.current?.scrollIntoView({ block: "center" });
|
||||
}, [toc]);
|
||||
if (nav === "toc") activeRow.current?.scrollIntoView({ block: "center" });
|
||||
}, [nav]);
|
||||
|
||||
function scrollToPage(i: number, smooth: boolean) {
|
||||
const el = boxRef.current;
|
||||
@@ -358,10 +358,22 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
|
||||
预读 {prefetch} 章
|
||||
</button>
|
||||
)}
|
||||
{bm.btn}
|
||||
<button
|
||||
className="rd-btn"
|
||||
aria-expanded={nav === "bm"}
|
||||
aria-controls="reader-nav"
|
||||
onClick={() => setNav(nav === "bm" ? null : "bm")}
|
||||
>
|
||||
书签
|
||||
</button>
|
||||
{chapters && (
|
||||
<>
|
||||
<button className="rd-btn" aria-expanded={toc} aria-controls="cbz-toc" onClick={() => setToc((v) => !v)}>
|
||||
<button
|
||||
className="rd-btn"
|
||||
aria-expanded={nav === "toc"}
|
||||
aria-controls="reader-nav"
|
||||
onClick={() => setNav(nav === "toc" ? null : "toc")}
|
||||
>
|
||||
目录
|
||||
</button>
|
||||
<span className="text-xs tabular-nums opacity-60">
|
||||
@@ -389,34 +401,43 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
|
||||
<span className="opacity-60">{Math.round(((cur + 1) / Math.max(1, count)) * 100)}%</span>
|
||||
</div>
|
||||
)}
|
||||
{toc && chapters && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-10" aria-hidden="true" onClick={() => setToc(false)} />
|
||||
<nav
|
||||
id="cbz-toc"
|
||||
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={() => {
|
||||
goPage(c.start);
|
||||
setToc(false);
|
||||
}}
|
||||
>
|
||||
{i + 1}. {c.title}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</>
|
||||
{nav && (
|
||||
<ReaderNav
|
||||
tab={chapters ? nav : "bm"}
|
||||
onTab={setNav}
|
||||
onClose={() => setNav(null)}
|
||||
bookmarks={
|
||||
<BookmarksTab
|
||||
book={book}
|
||||
capture={saver.capture}
|
||||
seek={(l) => {
|
||||
goPage(Number(l.page) || 0);
|
||||
setNav(null);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
toc={
|
||||
chapters && (
|
||||
<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={() => {
|
||||
goPage(c.start);
|
||||
setNav(null);
|
||||
}}
|
||||
>
|
||||
{i + 1}. {c.title}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{bm.panel}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user