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;
|
return 1;
|
||||||
});
|
});
|
||||||
const currentRatioRef = useRef(0);
|
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 imagesRef = useRef(images);
|
||||||
const chapterMetasRef = useRef(chapterMetas);
|
const chapterMetasRef = useRef(chapterMetas);
|
||||||
@ -326,9 +332,9 @@ export function PageReader({
|
|||||||
const scrollToResume = () => {
|
const scrollToResume = () => {
|
||||||
const el = pageElRef.current.get(pageKey(startChapterNumber, p.page));
|
const el = pageElRef.current.get(pageKey(startChapterNumber, p.page));
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
instantTop(
|
const rect = el.getBoundingClientRect();
|
||||||
scrollOffsetFromRatio(el.offsetTop, el.offsetHeight, p.ratio)
|
const docTop = rect.top + window.scrollY;
|
||||||
);
|
instantTop(scrollOffsetFromRatio(docTop, rect.height, p.ratio));
|
||||||
};
|
};
|
||||||
scrollToResume();
|
scrollToResume();
|
||||||
requestAnimationFrame(scrollToResume);
|
requestAnimationFrame(scrollToResume);
|
||||||
@ -343,29 +349,37 @@ export function PageReader({
|
|||||||
hiddenByScrollRef.current = true;
|
hiddenByScrollRef.current = true;
|
||||||
setShowUI(false);
|
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 bestCh = 0;
|
||||||
let bestPg = 0;
|
let bestPg = 0;
|
||||||
let bestTop = -1;
|
let bestTop = Number.NEGATIVE_INFINITY;
|
||||||
let bestEl: HTMLDivElement | null = null;
|
let bestHeight = 0;
|
||||||
for (const { chNum, pNum, el } of intersectingPagesRef.current.values()) {
|
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) {
|
if (top <= y + 80 && top > bestTop) {
|
||||||
bestTop = top;
|
bestTop = top;
|
||||||
|
bestHeight = rect.height;
|
||||||
bestCh = chNum;
|
bestCh = chNum;
|
||||||
bestPg = pNum;
|
bestPg = pNum;
|
||||||
bestEl = el;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!bestEl) return;
|
if (bestCh === 0) return;
|
||||||
currentRatioRef.current = calcScrollRatio(
|
hasScrolledRef.current = true;
|
||||||
y,
|
currentRatioRef.current = calcScrollRatio(y, bestTop, bestHeight);
|
||||||
bestTop,
|
currentChapterNumRef.current = bestCh;
|
||||||
bestEl.offsetHeight
|
currentPageNumRef.current = bestPg;
|
||||||
);
|
|
||||||
setCurrentChapterNum(bestCh);
|
setCurrentChapterNum(bestCh);
|
||||||
setCurrentPageNum(bestPg);
|
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 = () => {
|
const onScroll = () => {
|
||||||
if (rafId) return;
|
if (rafId) return;
|
||||||
@ -376,15 +390,31 @@ export function PageReader({
|
|||||||
window.removeEventListener("scroll", onScroll);
|
window.removeEventListener("scroll", onScroll);
|
||||||
if (rafId) cancelAnimationFrame(rafId);
|
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(() => {
|
useEffect(() => {
|
||||||
writeProgress(mangaSlug, {
|
const flush = () => {
|
||||||
chapter: currentChapterNum,
|
if (!hasScrolledRef.current) return;
|
||||||
page: currentPageNum,
|
writeProgress(mangaSlug, {
|
||||||
ratio: currentRatioRef.current,
|
chapter: currentChapterNumRef.current,
|
||||||
});
|
page: currentPageNumRef.current,
|
||||||
}, [mangaSlug, currentChapterNum, currentPageNum]);
|
ratio: currentRatioRef.current,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
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
|
// Aspect-ratio placeholders stay so layout is preserved; observer
|
||||||
// re-fetches images on scrollback into an unmounted chapter.
|
// re-fetches images on scrollback into an unmounted chapter.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user