diff --git a/components/PageReader.tsx b/components/PageReader.tsx index 3036e1a..d76efcc 100644 --- a/components/PageReader.tsx +++ b/components/PageReader.tsx @@ -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(() => { - writeProgress(mangaSlug, { - chapter: currentChapterNum, - page: currentPageNum, - ratio: currentRatioRef.current, - }); - }, [mangaSlug, currentChapterNum, currentPageNum]); + const flush = () => { + if (!hasScrolledRef.current) return; + writeProgress(mangaSlug, { + chapter: currentChapterNumRef.current, + page: currentPageNumRef.current, + 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 // re-fetches images on scrollback into an unmounted chapter.