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() {
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;
});
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],
);
const locked = !!chapters && chapters.length >= 2 && !continuous;
// 锁章下窗外前缀页不渲染 → scrollTop/页 top 一律窗口相对;读 ph(全局前缀和)前先加回 anchor
const anchor = locked ? ph.offset(w.start) : 0;
// 容器尺寸自适应:回调 ref 在滚动盒真正挂载时才观测(等页数请求期间的早退渲染里没有这个节点)
const roRef = useRef<ResizeObserver | null>(null);
@@ -136,7 +140,7 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
if (!el) return;
const t = pending.current;
pending.current = null;
el.scrollTop = ph.offset(t);
el.scrollTop = ph.offset(t) - anchor;
}, [ci, ph]);
// 滚动事件按帧合并;预取等滚动停稳再做——拖拽途中逐事件拉图=带宽/解码风暴
@@ -149,7 +153,7 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
const t = el.scrollTop;
setTop(t);
if (count) {
curRef.current = ph.pageAt(t + vh / 2);
curRef.current = ph.pageAt(t + anchor + vh / 2);
if (!locked) setCi(chapterIndexAt(chapters, curRef.current)); // 连读/扁平:当前章随滚动走(目录计数高亮)
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(() => {
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) {
const el = boxRef.current;
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 [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 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) {
const el = boxRef.current;
const topY = el ? el.scrollTop : 0;
const topY = el ? el.scrollTop + anchor : 0; // 与 ph.offset 同处全局空间比较
const oldEst = ph.estHeight;
const topOfI = ph.offset(i);
const d = ph.set(i, h);
@@ -247,7 +254,9 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
// 让未量高页的估高收敛到实测页高:同开本漫画量一页后 d≈0,拖拽滚动条不再逐帧打架
if (width > 0 && Math.abs(h - oldEst) / oldEst > 0.03) {
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;
}
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) => {
const i = a + k;
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)} />
</div>
);
@@ -278,7 +287,7 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
{locked && (
<div
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 ? (
<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) => {
const i = w.end + k;
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)} />
</div>
);