Reader: make position tracking accurate to sub-200ms
- Throttled write inside the scroll rAF tick (every 200ms during active scroll) captures mid-page ratio changes. Previously only page-boundary crossings triggered writes, so refreshing mid-page restored to top of that page instead of exact position. - pagehide + visibilitychange + unmount flush captures the final 0-200ms before tab close / bfcache / nav. - hasScrolledRef guards writes so opening a chapter without scrolling doesn't clobber a prior deep-progress bookmark for a different chapter. - getBoundingClientRect replaces offsetTop/offsetHeight for subpixel precision and positioning-ancestor independence. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b993de43bc
commit
cd1fd6ad64
@ -76,6 +76,12 @@ export function PageReader({
|
||||
return 1;
|
||||
});
|
||||
const currentRatioRef = useRef(0);
|
||||
const currentChapterNumRef = useRef(startChapterNumber);
|
||||
const currentPageNumRef = useRef(1);
|
||||
const lastWriteAtRef = useRef(0);
|
||||
// Guards progress writes so an empty session (opened chapter, never
|
||||
// scrolled) doesn't overwrite prior bookmark for a different chapter.
|
||||
const hasScrolledRef = useRef(false);
|
||||
|
||||
const imagesRef = useRef(images);
|
||||
const chapterMetasRef = useRef(chapterMetas);
|
||||
@ -326,9 +332,9 @@ export function PageReader({
|
||||
const scrollToResume = () => {
|
||||
const el = pageElRef.current.get(pageKey(startChapterNumber, p.page));
|
||||
if (!el) return;
|
||||
instantTop(
|
||||
scrollOffsetFromRatio(el.offsetTop, el.offsetHeight, p.ratio)
|
||||
);
|
||||
const rect = el.getBoundingClientRect();
|
||||
const docTop = rect.top + window.scrollY;
|
||||
instantTop(scrollOffsetFromRatio(docTop, rect.height, p.ratio));
|
||||
};
|
||||
scrollToResume();
|
||||
requestAnimationFrame(scrollToResume);
|
||||
@ -343,29 +349,37 @@ export function PageReader({
|
||||
hiddenByScrollRef.current = true;
|
||||
setShowUI(false);
|
||||
}
|
||||
// Pick the topmost page whose top edge is above y+80 (top edge of the
|
||||
// content below the sticky header); walking the small intersecting set.
|
||||
let bestCh = 0;
|
||||
let bestPg = 0;
|
||||
let bestTop = -1;
|
||||
let bestEl: HTMLDivElement | null = null;
|
||||
let bestTop = Number.NEGATIVE_INFINITY;
|
||||
let bestHeight = 0;
|
||||
for (const { chNum, pNum, el } of intersectingPagesRef.current.values()) {
|
||||
const top = el.offsetTop;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const top = rect.top + y;
|
||||
if (top <= y + 80 && top > bestTop) {
|
||||
bestTop = top;
|
||||
bestHeight = rect.height;
|
||||
bestCh = chNum;
|
||||
bestPg = pNum;
|
||||
bestEl = el;
|
||||
}
|
||||
}
|
||||
if (!bestEl) return;
|
||||
currentRatioRef.current = calcScrollRatio(
|
||||
y,
|
||||
bestTop,
|
||||
bestEl.offsetHeight
|
||||
);
|
||||
if (bestCh === 0) return;
|
||||
hasScrolledRef.current = true;
|
||||
currentRatioRef.current = calcScrollRatio(y, bestTop, bestHeight);
|
||||
currentChapterNumRef.current = bestCh;
|
||||
currentPageNumRef.current = bestPg;
|
||||
setCurrentChapterNum(bestCh);
|
||||
setCurrentPageNum(bestPg);
|
||||
|
||||
const now = performance.now();
|
||||
if (now - lastWriteAtRef.current >= 200) {
|
||||
lastWriteAtRef.current = now;
|
||||
writeProgress(mangaSlug, {
|
||||
chapter: bestCh,
|
||||
page: bestPg,
|
||||
ratio: currentRatioRef.current,
|
||||
});
|
||||
}
|
||||
};
|
||||
const onScroll = () => {
|
||||
if (rafId) return;
|
||||
@ -376,15 +390,31 @@ export function PageReader({
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
};
|
||||
}, []);
|
||||
}, [mangaSlug]);
|
||||
|
||||
// Flush latest scroll position on pagehide / visibilitychange / unmount
|
||||
// — catches the 0-200ms window between the last throttled tick-write
|
||||
// and tab close / bfcache.
|
||||
useEffect(() => {
|
||||
const flush = () => {
|
||||
if (!hasScrolledRef.current) return;
|
||||
writeProgress(mangaSlug, {
|
||||
chapter: currentChapterNum,
|
||||
page: currentPageNum,
|
||||
chapter: currentChapterNumRef.current,
|
||||
page: currentPageNumRef.current,
|
||||
ratio: currentRatioRef.current,
|
||||
});
|
||||
}, [mangaSlug, currentChapterNum, currentPageNum]);
|
||||
};
|
||||
const onVisibility = () => {
|
||||
if (document.visibilityState === "hidden") flush();
|
||||
};
|
||||
window.addEventListener("pagehide", flush);
|
||||
document.addEventListener("visibilitychange", onVisibility);
|
||||
return () => {
|
||||
flush();
|
||||
window.removeEventListener("pagehide", flush);
|
||||
document.removeEventListener("visibilitychange", onVisibility);
|
||||
};
|
||||
}, [mangaSlug]);
|
||||
|
||||
// Aspect-ratio placeholders stay so layout is preserved; observer
|
||||
// re-fetches images on scrollback into an unmounted chapter.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user