fix(cbz): clamp locked cur at chapter end, hide 连读 on flat packs, silent restore, tighten prefetch storage

This commit is contained in:
2026-09-08 15:28:27 +08:00
parent 60c4ccf26f
commit e5dacc0a77
+20 -15
View File
@@ -22,7 +22,7 @@ function useStoredBool(key: string, def: boolean) {
function useStoredPrefetch() { function useStoredPrefetch() {
const [n, setN] = useState(() => { const [n, setN] = useState(() => {
const s = localStorage.getItem("cbz-prefetch"); const s = localStorage.getItem("cbz-prefetch");
if (s === null) return 2; // 键缺失 ≠ 0:新用户默认 2 if (s === null || s === "") return 2; // 键缺失/空串 ≠ 0:新用户默认 2
const v = Number(s); const v = Number(s);
return Number.isInteger(v) && v >= 0 && v <= 3 ? v : 2; return Number.isInteger(v) && v >= 0 && v <= 3 ? v : 2;
}); });
@@ -130,7 +130,7 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
if (restored.current || !count || !boxRef.current) return; if (restored.current || !count || !boxRef.current) return;
restored.current = true; restored.current = true;
const p = Number(initialLocator?.page); const p = Number(initialLocator?.page);
if (Number.isInteger(p) && p > 0 && p < count) goPage(p, false); if (Number.isInteger(p) && p > 0 && p < count) goPage(p, false, false);
}, [count, initialLocator, ph]); }, [count, initialLocator, ph]);
// 跨章跳页:新窗口几何渲染后才能真正滚动(旧窗口 contentH 会把 scrollTop 夹断),故挂 pending 等 [ci] 生效 // 跨章跳页:新窗口几何渲染后才能真正滚动(旧窗口 contentH 会把 scrollTop 夹断),故挂 pending 等 [ci] 生效
@@ -154,6 +154,7 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
setTop(t); setTop(t);
if (count) { if (count) {
curRef.current = ph.pageAt(t + anchor + vh / 2); curRef.current = ph.pageAt(t + anchor + vh / 2);
if (locked) curRef.current = Math.min(curRef.current, w.end - 1); // 末屏中心落在章末卡片/他章 → 钉在本章末页
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);
} }
@@ -185,7 +186,8 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
}); });
} }
const cur = ph.pageAt(top + anchor + vh / 2); const curRaw = ph.pageAt(top + anchor + vh / 2);
const cur = locked ? Math.min(curRaw, w.end - 1) : curRaw; // 锁章:视口落在章末卡片上时读数不越章(HUD/滑条/进度)
useEffect(() => { useEffect(() => {
if (toc) activeRow.current?.scrollIntoView({ block: "center" }); if (toc) activeRow.current?.scrollIntoView({ block: "center" });
@@ -200,10 +202,10 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
}); });
} }
// 统一定位入口:锁章下目标页在窗外 = 显式切章(滑条/书签/目录/章末卡片/恢复进度皆此一路径) // 统一定位入口:锁章下目标页在窗外 = 显式切章(滑条/书签/目录/章末卡片皆此一路径);恢复进度走 show=false 静默落位
function goPage(i: number, smooth = true) { function goPage(i: number, smooth = true, show = true) {
const t = Math.max(0, Math.min(count - 1, i)); const t = Math.max(0, Math.min(count - 1, i));
chrome.show(); if (show) chrome.show();
if (locked && (t < w.start || t >= w.end)) { if (locked && (t < w.start || t >= w.end)) {
setCi(chapterIndexAt(chapters, t)); setCi(chapterIndexAt(chapters, t));
pending.current = t; pending.current = t;
@@ -253,9 +255,10 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
if (el && d !== 0 && topOfI < topY) applyShift(d); // 视口上方页高变化 → 补偿滚动 if (el && d !== 0 && topOfI < topY) applyShift(d); // 视口上方页高变化 → 补偿滚动
// 让未量高页的估高收敛到实测页高:同开本漫画量一页后 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 a0 = ph.offset(w.start); // 基线取当前实际 anchor:连续 measure 交错时闭包 anchor 可能过期,防前缀份额双扣
const n = ph.setEst(h, topY); const n = ph.setEst(h, topY);
// n×Δest 是全局位移;锁章下 anchor 同步变了(前缀窗外页),那部分不影响窗口相对几何,须扣减 // n×Δest 是全局位移;锁章下窗外前缀页的那部分不影响窗口相对几何,须扣减
const corr = n * (h - oldEst) - (ph.offset(w.start) - anchor); const corr = n * (h - oldEst) - (ph.offset(w.start) - a0);
if (el && corr) applyShift(corr); if (el && corr) applyShift(corr);
ratio.current = h / width; ratio.current = h / width;
} }
@@ -337,13 +340,15 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
</span> </span>
</div> </div>
<div className="mt-1 flex items-center gap-2 pb-1 text-sm"> <div className="mt-1 flex items-center gap-2 pb-1 text-sm">
<button {chapters && chapters.length >= 2 && (
className={"rd-btn" + (continuous ? " rd-btn-on" : "")} <button
aria-pressed={continuous} className={"rd-btn" + (continuous ? " rd-btn-on" : "")}
onClick={() => setContinuous(!continuous)} aria-pressed={continuous}
> onClick={() => setContinuous(!continuous)}
连读{continuous ? "·开" : "·关"} >
</button> 连读{continuous ? "·开" : "·关"}
</button>
)}
{locked && ( {locked && (
<button <button
className="rd-btn" className="rd-btn"