fix(web): stop CBZ reader snapping/bouncing at chapter end on mixed-orientation books
This commit is contained in:
@@ -58,6 +58,9 @@ The format loosely follows Keep a Changelog and can be adapted to the team's hab
|
|||||||
|
|
||||||
### Fixed / 修复
|
### Fixed / 修复
|
||||||
|
|
||||||
|
- CBZ reader no longer snaps to the top (or visibly bounces) when scrolling or dragging the scrollbar near the end of a long chapter: re-estimating every unmeasured page from each newly measured page made the scroll geometry oscillate on books with mixed page orientations (and the correction was computed from a stale anchor), so the estimator now learns once from the first measured page and then stays put; after a geometry change the viewport is re-anchored absolutely (keep the current page under the same screen spot) instead of by a delta, and while the scrollbar is being dragged the reader doesn't write scrollTop at all, leaving the browser in sole control.
|
||||||
|
- CBZ 阅读器在长章节末尾滚动或拖滚动条时不再出现"到底后被重置到上方"的跳变:此前每量完一页就用它重估所有未量页的高度,横竖版混排的书里估高来回翻转、滚动几何剧烈呼吸,且补偿量基于已过期的 anchor 计算;现改为估高只从首个实测页学习一次、之后保持稳定,几何变化后按"当前页钉回原屏幕位置"绝对重锚(不再用增量补偿),拖拽滚动条期间阅读器完全不写 scrollTop,避免与浏览器抢滚动控制权。
|
||||||
|
|
||||||
- Library upload now reports the real file count: the success toast read the live `FileList` after the file input had been reset (always 0, e.g. "已上传 0 个文件"), making successful uploads look broken while waiting for the scanner; the list is snapshotted before uploading.
|
- Library upload now reports the real file count: the success toast read the live `FileList` after the file input had been reset (always 0, e.g. "已上传 0 个文件"), making successful uploads look broken while waiting for the scanner; the list is snapshotted before uploading.
|
||||||
- 库管理上传完成提示不再恒为「已上传 0 个文件」:此前成功提示在清空 file input 之后读取其活引用 FileList 的长度(始终为 0),上传明明成功却像没生效;现在在上传开始前对文件列表做快照,计数与遍历都用快照。
|
- 库管理上传完成提示不再恒为「已上传 0 个文件」:此前成功提示在清空 file input 之后读取其活引用 FileList 的长度(始终为 0),上传明明成功却像没生效;现在在上传开始前对文件列表做快照,计数与遍历都用快照。
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
|
|||||||
const curRef = useRef(0);
|
const curRef = useRef(0);
|
||||||
const shift = useRef(0); // 量高补偿按帧合并,避免一次加载多页各改一次 scrollTop
|
const shift = useRef(0); // 量高补偿按帧合并,避免一次加载多页各改一次 scrollTop
|
||||||
const shiftRaf = useRef(0);
|
const shiftRaf = useRef(0);
|
||||||
|
const dragging = useRef(false); // 拖滚动条中:scrollTop 归浏览器管,JS 只补几何不抢方向盘
|
||||||
|
const estLearned = useRef(false); // 估高只学一次(见 measure)
|
||||||
const ratio = useRef(EST_RATIO); // 学到的页高比(同开本漫画一页量准,全程几何稳定)
|
const ratio = useRef(EST_RATIO); // 学到的页高比(同开本漫画一页量准,全程几何稳定)
|
||||||
const [continuous, setContinuous] = useStoredBool("cbz-continuous", false); // 连读:滑动可跨章
|
const [continuous, setContinuous] = useStoredBool("cbz-continuous", false); // 连读:滑动可跨章
|
||||||
const [prefetch, setPrefetch] = useStoredPrefetch(); // 锁章预读章数 0–3
|
const [prefetch, setPrefetch] = useStoredPrefetch(); // 锁章预读章数 0–3
|
||||||
@@ -178,7 +180,18 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const up = () => (dragging.current = false);
|
||||||
|
window.addEventListener("pointerup", up);
|
||||||
|
window.addEventListener("pointercancel", up);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("pointerup", up);
|
||||||
|
window.removeEventListener("pointercancel", up);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
function applyShift(d: number) {
|
function applyShift(d: number) {
|
||||||
|
if (dragging.current) return; // 拖拽中 JS 写 scrollTop 会和浏览器"滑块位置→滚动"映射抢方向盘
|
||||||
shift.current += d;
|
shift.current += d;
|
||||||
if (shiftRaf.current) return;
|
if (shiftRaf.current) return;
|
||||||
shiftRaf.current = requestAnimationFrame(() => {
|
shiftRaf.current = requestAnimationFrame(() => {
|
||||||
@@ -251,18 +264,29 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
|
|||||||
|
|
||||||
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 + anchor : 0; // 与 ph.offset 同处全局空间比较
|
// anchor 一律现取:setEst 会改写窗外未量页前缀的高度,渲染闭包里的 anchor 同帧内即过期,
|
||||||
const oldEst = ph.estHeight;
|
// 拿它换算 topY → keep 页算错数十页 → 重锚把视口弹飞 → 新页量高再触发翻转 = 章尾震荡
|
||||||
|
const aNow = ph.offset(w.start);
|
||||||
|
const topY = el ? el.scrollTop + aNow : 0; // 与 ph.offset 同处全局空间比较
|
||||||
const topOfI = ph.offset(i);
|
const topOfI = ph.offset(i);
|
||||||
const d = ph.set(i, h);
|
const d = ph.set(i, h);
|
||||||
if (el && d !== 0 && topOfI < topY) applyShift(d); // 视口上方页高变化 → 补偿滚动
|
if (el && d !== 0 && topOfI < topY) applyShift(d); // 视口上方页高变化 → 补偿滚动
|
||||||
// 让未量高页的估高收敛到实测页高:同开本漫画量一页后 d≈0,拖拽滚动条不再逐帧打架
|
// 让未量高页的估高收敛到实测页高。重估瞬间改写 contentH,浏览器会先把 scrollTop 夹断,
|
||||||
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);
|
// 估高只学一次:混开本(横竖版混排)书里"按最近一页重估"会让 est 反复翻转,
|
||||||
// n×Δest 是全局位移;锁章下窗外前缀页的那部分不影响窗口相对几何,须扣减
|
// 每次翻转都改写全部未量页几何(contentH ±16k 呼吸),拖滑块到底就是在这上面震荡。
|
||||||
const corr = n * (h - oldEst) - (ph.offset(w.start) - a0);
|
// 学一次钉死后,后续每页量高只动自己那一格(≤单页高),几何单调稳定。
|
||||||
if (el && corr) applyShift(corr);
|
if (width > 0 && !estLearned.current) {
|
||||||
|
estLearned.current = true;
|
||||||
|
const keep = el ? ph.pageAt(topY) : 0;
|
||||||
|
let keepIn = el ? topY - ph.offset(keep) : 0;
|
||||||
|
ph.setEst(h, topY);
|
||||||
|
if (el && !dragging.current) {
|
||||||
|
keepIn = Math.min(keepIn, Math.max(0, ph.offset(keep + 1) - ph.offset(keep)));
|
||||||
|
shift.current = 0; // 增量补偿作废,重锚已含全部几何变化
|
||||||
|
el.scrollTop = ph.offset(keep) - ph.offset(w.start) + keepIn;
|
||||||
|
}
|
||||||
ratio.current = h / width;
|
ratio.current = h / width;
|
||||||
}
|
}
|
||||||
bump((x) => x + 1);
|
bump((x) => x + 1);
|
||||||
@@ -280,7 +304,17 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div ref={attachBox} onScroll={onScroll} onClick={onZoneClick} className="h-full overflow-y-auto">
|
<div
|
||||||
|
ref={attachBox}
|
||||||
|
onScroll={onScroll}
|
||||||
|
onClick={onZoneClick}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
const el = e.currentTarget;
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
if (e.clientX - r.left >= el.clientWidth || e.clientY - r.top >= el.clientHeight) dragging.current = true; // 按在滚动条上
|
||||||
|
}}
|
||||||
|
className="h-full overflow-y-auto"
|
||||||
|
>
|
||||||
<div className="relative mx-auto" style={{ height: contentH, maxWidth: MAX_W }}>
|
<div className="relative mx-auto" style={{ height: contentH, maxWidth: MAX_W }}>
|
||||||
{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;
|
||||||
|
|||||||
Reference in New Issue
Block a user