fix(cbz): window-relative scroll geometry locks head boundary; default prefetch 2 when key absent
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user