feat(cbz): adapt reader to real archive structure — PageIndex skips macOS junk (__MACOSX/ and ._ AppleDouble were polluting pages with 163-byte blacks), pages endpoint derives chapters[{title,start}] from folder layout, imageless zip now lands state=error instead of an empty reader, CbzReader gains 目录 sidebar + prev/next chapter (TextReader pattern, rd-* classes); pagesidx cache key bumped for one-time invalidation; e2e on 311MB 調教開關:第二季.zip: 15048→7524 real pages, 52 chapters, all JPEGs
This commit is contained in:
@@ -172,7 +172,8 @@ export const api = {
|
||||
},
|
||||
getBook: (id: number) => apiFetch<Book>(`/books/${id}`),
|
||||
deleteBook: (id: number) => apiFetch<void>(`/books/${id}`, { method: "DELETE" }),
|
||||
pageCount: (pagesUrl: string) => apiFetch<{ count: number }>(pagesUrl),
|
||||
pageCount: (pagesUrl: string) =>
|
||||
apiFetch<{ count: number; chapters?: { title: string; start: number }[] | null }>(pagesUrl),
|
||||
|
||||
putProgress: (id: number, locator: Record<string, unknown>, percent: number, keepalive = false) =>
|
||||
apiFetch<void>(`/books/${id}/progress`, { method: "PUT", body: { locator, percent }, keepalive }),
|
||||
|
||||
@@ -84,6 +84,9 @@ export default function CbzReader({ book, initialLocator, chrome }: ReaderProps)
|
||||
retry: 0,
|
||||
});
|
||||
const count = countQ.data?.count ?? 0;
|
||||
const chapters = countQ.data?.chapters ?? null; // 包内目录结构(第N話/上中下卷)
|
||||
const [toc, setToc] = useState(false);
|
||||
const activeRow = useRef<HTMLButtonElement>(null);
|
||||
const saver = useProgressSaver(book.id);
|
||||
const boxRef = useRef<HTMLDivElement>(null);
|
||||
const [width, setWidth] = useState(480);
|
||||
@@ -168,6 +171,10 @@ export default function CbzReader({ book, initialLocator, chrome }: ReaderProps)
|
||||
}
|
||||
|
||||
const cur = ph.pageAt(top + vh / 2);
|
||||
const ci = chapters ? chapters.reduce((acc, c, i) => (c.start <= cur ? i : acc), 0) : 0;
|
||||
useEffect(() => {
|
||||
if (toc) activeRow.current?.scrollIntoView({ block: "center" });
|
||||
}, [toc]);
|
||||
|
||||
function jump(i: number, smooth = true) {
|
||||
const el = boxRef.current;
|
||||
@@ -284,6 +291,26 @@ export default function CbzReader({ book, initialLocator, chrome }: ReaderProps)
|
||||
>
|
||||
{MODE_LABEL[mode]}
|
||||
</button>
|
||||
{chapters && (
|
||||
<>
|
||||
<button className="rd-btn" aria-expanded={toc} aria-controls="cbz-toc" onClick={() => setToc((v) => !v)}>
|
||||
目录
|
||||
</button>
|
||||
<span className="text-xs tabular-nums opacity-60">
|
||||
{ci + 1}/{chapters.length}
|
||||
</span>
|
||||
<button className="rd-btn" disabled={ci === 0} onClick={() => jump(chapters[ci - 1].start)}>
|
||||
← 上一章
|
||||
</button>
|
||||
<button
|
||||
className="rd-btn"
|
||||
disabled={ci === chapters.length - 1}
|
||||
onClick={() => jump(chapters[ci + 1].start)}
|
||||
>
|
||||
下一章 →
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -294,6 +321,33 @@ export default function CbzReader({ book, initialLocator, chrome }: ReaderProps)
|
||||
<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={() => {
|
||||
jump(c.start);
|
||||
setToc(false);
|
||||
}}
|
||||
>
|
||||
{i + 1}. {c.title}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user