yiekheng 95135942a2 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>
2026-04-15 22:29:14 +08:00

79 lines
2.0 KiB
TypeScript

export type ReadingProgress = {
chapter: number;
page: number;
ratio: number;
};
export type StorageLike = Pick<Storage, "getItem" | "setItem">;
export function storageKey(slug: string): string {
return `sunnymh:last-read:${slug}`;
}
function clampRatio(n: unknown): number {
const v = typeof n === "number" ? n : Number(n);
if (!Number.isFinite(v)) return 0;
if (v < 0) return 0;
if (v > 1) return 1;
return v;
}
export function parseProgress(raw: string | null): ReadingProgress | null {
if (!raw) return null;
if (raw.startsWith("{")) {
try {
const parsed = JSON.parse(raw) as Partial<ReadingProgress>;
if (
typeof parsed.chapter === "number" &&
typeof parsed.page === "number" &&
parsed.chapter > 0 &&
parsed.page > 0
) {
return {
chapter: parsed.chapter,
page: parsed.page,
ratio: clampRatio(parsed.ratio),
};
}
} catch {
return null;
}
return null;
}
const n = Number(raw);
return Number.isFinite(n) && n > 0
? { chapter: n, page: 1, ratio: 0 }
: null;
}
export function readProgress(
slug: string,
storage: StorageLike | null = defaultStorage()
): ReadingProgress | null {
if (!storage) return null;
return parseProgress(storage.getItem(storageKey(slug)));
}
export function writeProgress(
slug: string,
progress: ReadingProgress,
storage: StorageLike | null = defaultStorage()
): void {
if (!storage) return;
storage.setItem(storageKey(slug), JSON.stringify(progress));
}
function defaultStorage(): StorageLike | null {
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"
);
}