Reader: treat browser refresh as implicit resume

Refreshing the chapter page was landing the user back at the top, because
only ?resume=1 triggered the saved-position restore. Added isPageReload()
helper (checks performance navigation entry type === 'reload') and OR'd
it with the resume flag. Refresh now restores to the last scroll
position; drawer/list clicks still go to top as intended.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
yiekheng 2026-04-15 22:29:14 +08:00
parent fb2b032d73
commit 95135942a2
2 changed files with 18 additions and 4 deletions

View File

@ -15,6 +15,7 @@ import {
readProgress, readProgress,
writeProgress, writeProgress,
} from "@/components/ReadingProgressButton"; } from "@/components/ReadingProgressButton";
import { isPageReload } from "@/lib/progress";
import { LoadingLogo } from "@/components/LoadingLogo"; import { LoadingLogo } from "@/components/LoadingLogo";
import { import {
calcScrollRatio, calcScrollRatio,
@ -70,7 +71,8 @@ export function PageReader({
const [currentChapterNum, setCurrentChapterNum] = const [currentChapterNum, setCurrentChapterNum] =
useState(startChapterNumber); useState(startChapterNumber);
const [currentPageNum, setCurrentPageNum] = useState(() => { const [currentPageNum, setCurrentPageNum] = useState(() => {
if (typeof window === "undefined" || !resume) return 1; if (typeof window === "undefined") return 1;
if (!resume && !isPageReload()) return 1;
const p = readProgress(mangaSlug); const p = readProgress(mangaSlug);
if (p && p.chapter === startChapterNumber && p.page > 1) return p.page; if (p && p.chapter === startChapterNumber && p.page > 1) return p.page;
return 1; return 1;
@ -322,15 +324,17 @@ export function PageReader({
// All reader Links use scroll={false} to preserve scroll during in-reader // All reader Links use scroll={false} to preserve scroll during in-reader
// nav (natural scroll between chapters updates URL without remount). On // nav (natural scroll between chapters updates URL without remount). On
// a fresh mount we must actively position the scroll: resume-to-saved // a fresh mount we position scroll: resume-to-saved if ?resume=1 (from
// if ?resume=1 AND the saved chapter matches; otherwise top. // 继续阅读) OR a page reload (so browser refresh preserves position).
// Plain chapter-link clicks from drawer / list go to top.
const resumeDoneRef = useRef(false); const resumeDoneRef = useRef(false);
useLayoutEffect(() => { useLayoutEffect(() => {
if (resumeDoneRef.current) return; if (resumeDoneRef.current) return;
resumeDoneRef.current = true; resumeDoneRef.current = true;
const instantTop = (top: number) => const instantTop = (top: number) =>
window.scrollTo({ top, behavior: "instant" as ScrollBehavior }); window.scrollTo({ top, behavior: "instant" as ScrollBehavior });
if (!resume) { const shouldResume = resume || isPageReload();
if (!shouldResume) {
instantTop(0); instantTop(0);
return; return;
} }

View File

@ -66,3 +66,13 @@ export function writeProgress(
function defaultStorage(): StorageLike | null { function defaultStorage(): StorageLike | null {
return typeof window === "undefined" ? null : window.localStorage; return typeof window === "undefined" ? null : window.localStorage;
} }
export function isPageReload(): boolean {
if (typeof window === "undefined") return false;
if (typeof performance === "undefined") return false;
const entries = performance.getEntriesByType("navigation");
if (entries.length === 0) return false;
return (
(entries[0] as PerformanceNavigationTiming).type === "reload"
);
}