diff --git a/components/PageReader.tsx b/components/PageReader.tsx index dea9b6a..9270a2f 100644 --- a/components/PageReader.tsx +++ b/components/PageReader.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useRef, useCallback } from "react"; +import { useState, useEffect, useRef, useCallback, useMemo } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { @@ -84,6 +84,13 @@ export function PageReader({ const observerRef = useRef(null); const pageRefsRef = useRef>(new Map()); + // Reverse-fetch cursor: the offset of the first loaded page of the + // starting chapter. Used to prepend previous pages when the user scrolls + // up from a resumed mid-chapter position. + const prependOffsetRef = useRef(offsetRef.current); + const prependLoadingRef = useRef(false); + const prependExhaustedRef = useRef(offsetRef.current === 0); + const advanceChapterOrFinish = useCallback(() => { if (fetchChapterIdxRef.current + 1 < chapters.length) { fetchChapterIdxRef.current += 1; @@ -93,6 +100,71 @@ export function PageReader({ } }, [chapters.length]); + const startChapter = useMemo( + () => chapters.find((c) => c.number === startChapterNumber), + [chapters, startChapterNumber] + ); + + const prependBatch = useCallback(async () => { + if ( + prependLoadingRef.current || + prependExhaustedRef.current || + !startChapter + ) + return; + const currentOffset = prependOffsetRef.current; + const newOffset = Math.max(0, currentOffset - BATCH_SIZE); + const limit = currentOffset - newOffset; + if (limit <= 0) { + prependExhaustedRef.current = true; + return; + } + prependLoadingRef.current = true; + const beforeHeight = document.documentElement.scrollHeight; + try { + const res = await fetch( + `/api/pages?chapterId=${startChapter.id}&offset=${newOffset}&limit=${limit}` + ); + const batch: RawPage[] = await res.json(); + if (batch.length === 0) { + prependExhaustedRef.current = true; + return; + } + prependOffsetRef.current = newOffset; + if (newOffset === 0) prependExhaustedRef.current = true; + + // Shift forward-fetch trigger indices + total count since every + // already-loaded page has moved right by batch.length + const shift = batch.length; + const shifted = new Set(); + for (const t of triggerIndicesRef.current) shifted.add(t + shift); + triggerIndicesRef.current = shifted; + loadedCountRef.current += shift; + + setPages((prev) => [ + ...batch.map((p) => ({ + chapterNumber: startChapter.number, + pageNumber: p.number, + imageUrl: p.imageUrl, + })), + ...prev, + ]); + + // aspect-[3/4] on the prepended s reserves height before their + // bytes arrive, so scrollHeight is accurate immediately after React + // commits. One scrollBy keeps the previously-visible page anchored. + requestAnimationFrame(() => { + const afterHeight = document.documentElement.scrollHeight; + const delta = afterHeight - beforeHeight; + if (delta > 0) window.scrollBy({ top: delta }); + }); + } catch { + // ignore — user can scroll-up again to retry + } finally { + prependLoadingRef.current = false; + } + }, [startChapter]); + const fetchBatch = useCallback(async () => { if (loadingRef.current || doneRef.current) return; const chapter = chapters[fetchChapterIdxRef.current]; @@ -253,6 +325,15 @@ export function PageReader({ hiddenByScrollRef.current = true; setShowUI(false); } + // Trigger backward prepend when user approaches the top — fire early + // so the DOM/pics are already spawned by the time the user scrolls there + if ( + y < 2500 && + !prependLoadingRef.current && + !prependExhaustedRef.current + ) { + prependBatch(); + } // Nothing loaded yet — don't overwrite the resumed-page state if (pages.length === 0) return; let chapter = pages[0].chapterNumber; @@ -277,7 +358,7 @@ export function PageReader({ window.removeEventListener("scroll", onScroll); if (rafId) cancelAnimationFrame(rafId); }; - }, [pages, startChapterNumber]); + }, [pages, startChapterNumber, prependBatch]); // Persist progress as user scrolls. offsetRef has been pre-seeded above // so the first fetched page IS the resumed page — no scroll restoration @@ -377,7 +458,7 @@ export function PageReader({ {`Page