fix(cbz): window-relative scroll geometry locks head boundary; default prefetch 2 when key absent

This commit is contained in:
2026-09-08 15:15:42 +08:00
parent f63b639f64
commit 60c4ccf26f
+21 -12
View File
@@ -21,7 +21,9 @@ function useStoredBool(key: string, def: boolean) {
function useStoredPrefetch() { function useStoredPrefetch() {
const [n, setN] = useState(() => { const [n, setN] = useState(() => {
const v = Number(localStorage.getItem("cbz-prefetch")); const s = localStorage.getItem("cbz-prefetch");
if (s === null) return 2; // 键缺失 ≠ 0:新用户默认 2
const v = Number(s);
return Number.isInteger(v) && v >= 0 && v <= 3 ? v : 2; return Number.isInteger(v) && v >= 0 && v <= 3 ? v : 2;
}); });
useEffect(() => localStorage.setItem("cbz-prefetch", String(n)), [n]); useEffect(() => localStorage.setItem("cbz-prefetch", String(n)), [n]);
@@ -105,6 +107,8 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
[chapters, count, ci, continuous, prefetch], [chapters, count, ci, continuous, prefetch],
); );
const locked = !!chapters && chapters.length >= 2 && !continuous; const locked = !!chapters && chapters.length >= 2 && !continuous;
// 锁章下窗外前缀页不渲染 → scrollTop/页 top 一律窗口相对;读 ph(全局前缀和)前先加回 anchor
const anchor = locked ? ph.offset(w.start) : 0;
// 容器尺寸自适应:回调 ref 在滚动盒真正挂载时才观测(等页数请求期间的早退渲染里没有这个节点) // 容器尺寸自适应:回调 ref 在滚动盒真正挂载时才观测(等页数请求期间的早退渲染里没有这个节点)
const roRef = useRef<ResizeObserver | null>(null); const roRef = useRef<ResizeObserver | null>(null);
@@ -136,7 +140,7 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
if (!el) return; if (!el) return;
const t = pending.current; const t = pending.current;
pending.current = null; pending.current = null;
el.scrollTop = ph.offset(t); el.scrollTop = ph.offset(t) - anchor;
}, [ci, ph]); }, [ci, ph]);
// 滚动事件按帧合并;预取等滚动停稳再做——拖拽途中逐事件拉图=带宽/解码风暴 // 滚动事件按帧合并;预取等滚动停稳再做——拖拽途中逐事件拉图=带宽/解码风暴
@@ -149,7 +153,7 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
const t = el.scrollTop; const t = el.scrollTop;
setTop(t); setTop(t);
if (count) { if (count) {
curRef.current = ph.pageAt(t + vh / 2); curRef.current = ph.pageAt(t + anchor + vh / 2);
if (!locked) setCi(chapterIndexAt(chapters, curRef.current)); // 连读/扁平:当前章随滚动走(目录计数高亮) if (!locked) setCi(chapterIndexAt(chapters, curRef.current)); // 连读/扁平:当前章随滚动走(目录计数高亮)
saver.report({ page: curRef.current }, (curRef.current + 1) / count); saver.report({ page: curRef.current }, (curRef.current + 1) / count);
} }
@@ -181,7 +185,7 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
}); });
} }
const cur = ph.pageAt(top + vh / 2); const cur = ph.pageAt(top + anchor + vh / 2);
useEffect(() => { useEffect(() => {
if (toc) activeRow.current?.scrollIntoView({ block: "center" }); if (toc) activeRow.current?.scrollIntoView({ block: "center" });
@@ -190,7 +194,10 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
function scrollToPage(i: number, smooth: boolean) { function scrollToPage(i: number, smooth: boolean) {
const el = boxRef.current; const el = boxRef.current;
if (!el) return; if (!el) return;
el.scrollTo({ top: ph.offset(Math.max(w.start, Math.min(w.end - 1, i))), behavior: smooth ? "smooth" : "auto" }); el.scrollTo({
top: ph.offset(Math.max(w.start, Math.min(w.end - 1, i))) - anchor,
behavior: smooth ? "smooth" : "auto",
});
} }
// 统一定位入口:锁章下目标页在窗外 = 显式切章(滑条/书签/目录/章末卡片/恢复进度皆此一路径) // 统一定位入口:锁章下目标页在窗外 = 显式切章(滑条/书签/目录/章末卡片/恢复进度皆此一路径)
@@ -232,14 +239,14 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
); );
const fmt: string = book.page_url_fmt; const fmt: string = book.page_url_fmt;
const [a0, b0] = ph.range(top, vh, 2); const [a0, b0] = ph.range(top + anchor, vh, 2); // 视口位置换回全局空间再查页(锁章下 top 是窗口相对的)
const a = Math.max(a0, w.start); const a = Math.max(a0, w.start);
const b = Math.min(b0, w.end); // 可见页永不超过章末;窗外页在隐藏预读层 const b = Math.min(b0, w.end); // 可见页永不超过章末;窗外页在隐藏预读层
const contentH = locked ? ph.offset(w.end) + vh : ph.total(); const contentH = locked ? ph.offset(w.end) - anchor + vh : ph.total();
function measure(i: number, h: number) { function measure(i: number, h: number) {
const el = boxRef.current; const el = boxRef.current;
const topY = el ? el.scrollTop : 0; const topY = el ? el.scrollTop + anchor : 0; // 与 ph.offset 同处全局空间比较
const oldEst = ph.estHeight; const oldEst = ph.estHeight;
const topOfI = ph.offset(i); const topOfI = ph.offset(i);
const d = ph.set(i, h); const d = ph.set(i, h);
@@ -247,7 +254,9 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
// 让未量高页的估高收敛到实测页高:同开本漫画量一页后 d≈0,拖拽滚动条不再逐帧打架 // 让未量高页的估高收敛到实测页高:同开本漫画量一页后 d≈0,拖拽滚动条不再逐帧打架
if (width > 0 && Math.abs(h - oldEst) / oldEst > 0.03) { if (width > 0 && Math.abs(h - oldEst) / oldEst > 0.03) {
const n = ph.setEst(h, topY); const n = ph.setEst(h, topY);
if (el && n) applyShift(n * (h - oldEst)); // n×Δest 是全局位移;锁章下 anchor 同步变了(前缀窗外页),那部分不影响窗口相对几何,须扣减
const corr = n * (h - oldEst) - (ph.offset(w.start) - anchor);
if (el && corr) applyShift(corr);
ratio.current = h / width; ratio.current = h / width;
} }
bump((x) => x + 1); bump((x) => x + 1);
@@ -270,7 +279,7 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
{Array.from({ length: Math.max(0, b - a) }, (_, k) => { {Array.from({ length: Math.max(0, b - a) }, (_, k) => {
const i = a + k; const i = a + k;
return ( return (
<div key={i} className="absolute left-0 w-full" style={{ top: ph.offset(i) }}> <div key={i} className="absolute left-0 w-full" style={{ top: ph.offset(i) - anchor }}>
<PageImg url={formatPageUrl(fmt, i)} width={width} onLoaded={(h) => measure(i, h)} /> <PageImg url={formatPageUrl(fmt, i)} width={width} onLoaded={(h) => measure(i, h)} />
</div> </div>
); );
@@ -278,7 +287,7 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
{locked && ( {locked && (
<div <div
className="absolute left-0 grid w-full place-items-center" className="absolute left-0 grid w-full place-items-center"
style={{ top: ph.offset(w.end), height: vh }} style={{ top: ph.offset(w.end) - anchor, height: vh }}
> >
{w.end >= count ? ( {w.end >= count ? (
<span className="text-sm text-stone-500">— 全书完 —</span> <span className="text-sm text-stone-500">— 全书完 —</span>
@@ -295,7 +304,7 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
{Array.from({ length: w.mountEnd - w.end }, (_, k) => { {Array.from({ length: w.mountEnd - w.end }, (_, k) => {
const i = w.end + k; const i = w.end + k;
return ( return (
<div key={i} className="absolute left-0 w-full" style={{ top: ph.offset(i) }}> <div key={i} className="absolute left-0 w-full" style={{ top: ph.offset(i) - anchor }}>
<PageImg url={formatPageUrl(fmt, i)} width={width} onLoaded={(h) => measure(i, h)} /> <PageImg url={formatPageUrl(fmt, i)} width={width} onLoaded={(h) => measure(i, h)} />
</div> </div>
); );