修复因dc31a788fc7ee8b23f07f1cfdcd5748c26103c24 产生的bug

This commit is contained in:
mtvpls
2026-05-28 21:46:29 +08:00
parent cc98b21d66
commit 93ea31b546
+181 -23
View File
@@ -103,6 +103,7 @@ interface EpubBookInstance {
interface EpubRendition { interface EpubRendition {
display: (target?: string) => Promise<void>; display: (target?: string) => Promise<void>;
on: (event: 'relocated', callback: (location: EpubLocation) => void) => void; on: (event: 'relocated', callback: (location: EpubLocation) => void) => void;
currentLocation?: () => EpubLocation;
prev?: () => void; prev?: () => void;
next?: () => void; next?: () => void;
destroy?: () => void; destroy?: () => void;
@@ -469,6 +470,87 @@ function parseChapterScrollLocator(value?: string) {
return Math.max(0, Math.min(1, ratio)); return Math.max(0, Math.min(1, ratio));
} }
function getHrefFromScrollLocator(value?: string) {
if (!value || parseChapterScrollLocator(value) === null) return '';
return value.split('#scroll=')[0] || '';
}
function getScrolledPositionFromRecord(
record?: BookReadRecord | null
): ScrolledReadingPosition | null {
const ratio = parseChapterScrollLocator(record?.locator?.value);
if (ratio === null) return null;
const href =
record?.chapterHref ||
record?.locator?.href ||
getHrefFromScrollLocator(record?.locator?.value);
if (!href) return null;
return {
href,
scrollTop: ratio,
scrollHeight: 2,
clientHeight: 1,
updatedAt: record?.saveTime || Date.now(),
};
}
function getWindowScrollMetrics() {
if (typeof window === 'undefined' || typeof document === 'undefined')
return null;
const root = document.scrollingElement || document.documentElement;
const scrollTop = Math.max(
0,
window.scrollY || root?.scrollTop || document.body?.scrollTop || 0
);
const scrollHeight = Math.max(
root?.scrollHeight || 0,
document.documentElement?.scrollHeight || 0,
document.body?.scrollHeight || 0
);
const clientHeight =
root?.clientHeight || document.documentElement?.clientHeight || window.innerHeight || 0;
const maxScrollTop = Math.max(0, scrollHeight - clientHeight);
return { scrollTop, scrollHeight, clientHeight, maxScrollTop };
}
function getChapterScrollMetrics(node: HTMLDivElement | null) {
const nodeScrollHeight = node?.scrollHeight || 0;
const nodeClientHeight = node?.clientHeight || 0;
const nodeScrollTop = Math.max(0, node?.scrollTop || 0);
const nodeMaxScrollTop = Math.max(0, nodeScrollHeight - nodeClientHeight);
const windowMetrics = getWindowScrollMetrics();
if (
windowMetrics &&
windowMetrics.maxScrollTop > 0 &&
(windowMetrics.scrollTop > 0 || nodeMaxScrollTop <= 0)
) {
return windowMetrics;
}
return {
scrollTop: nodeScrollTop,
scrollHeight: nodeScrollHeight,
clientHeight: nodeClientHeight,
maxScrollTop: nodeMaxScrollTop,
};
}
function setChapterScrollTop(node: HTMLDivElement | null, top: number) {
const metrics = getChapterScrollMetrics(node);
const targetTop = Math.max(0, Math.min(metrics.maxScrollTop, top));
const windowMetrics = getWindowScrollMetrics();
if (
windowMetrics &&
metrics.scrollHeight === windowMetrics.scrollHeight &&
metrics.clientHeight === windowMetrics.clientHeight
) {
window.scrollTo({ top: targetTop, behavior: 'auto' });
return;
}
node?.scrollTo({ top: targetTop, behavior: 'auto' });
}
function flattenToc(items: TocItem[]): TocItem[] { function flattenToc(items: TocItem[]): TocItem[] {
return items.flatMap((item) => [item, ...flattenToc(item.subitems || [])]); return items.flatMap((item) => [item, ...flattenToc(item.subitems || [])]);
} }
@@ -594,6 +676,9 @@ function ChapterReader({ manifest }: { manifest: BookReadManifest }) {
const restoredChapterPositionRef = useRef(false); const restoredChapterPositionRef = useRef(false);
const chapterSaveTimerRef = useRef<number | null>(null); const chapterSaveTimerRef = useRef<number | null>(null);
const pendingChapterRestoreRatioRef = useRef<number | null>(null); const pendingChapterRestoreRatioRef = useRef<number | null>(null);
const lastChapterScrollMetricsRef = useRef<ReturnType<
typeof getChapterScrollMetrics
> | null>(null);
const currentIndexRef = useRef(0); const currentIndexRef = useRef(0);
const lastChapterSavedAtRef = useRef(0); const lastChapterSavedAtRef = useRef(0);
const lastChapterSavedLocatorValueRef = useRef( const lastChapterSavedLocatorValueRef = useRef(
@@ -735,12 +820,15 @@ function ChapterReader({ manifest }: { manifest: BookReadManifest }) {
const buildChapterReadRecord = useCallback( const buildChapterReadRecord = useCallback(
(item: BookChapter, index: number): BookReadRecord => { (item: BookChapter, index: number): BookReadRecord => {
const node = scrollRef.current; const currentMetrics = getChapterScrollMetrics(scrollRef.current);
const scrollTop = node?.scrollTop || 0; const savedMetrics = lastChapterScrollMetricsRef.current;
const scrollHeight = node?.scrollHeight || 0; const { scrollTop, scrollHeight, clientHeight, maxScrollTop } =
const clientHeight = node?.clientHeight || 0; currentMetrics.scrollTop === 0 &&
savedMetrics &&
savedMetrics.scrollTop > 0
? savedMetrics
: currentMetrics;
const chapterCount = Math.max(1, chapters.length); const chapterCount = Math.max(1, chapters.length);
const maxScrollTop = Math.max(0, scrollHeight - clientHeight);
const chapterRatio = const chapterRatio =
maxScrollTop > 0 maxScrollTop > 0
? Math.max(0, Math.min(1, scrollTop / maxScrollTop)) ? Math.max(0, Math.min(1, scrollTop / maxScrollTop))
@@ -861,7 +949,8 @@ function ChapterReader({ manifest }: { manifest: BookReadManifest }) {
stopTts(true); stopTts(true);
setLoading(true); setLoading(true);
setError(''); setError('');
scrollRef.current?.scrollTo({ top: 0, behavior: 'auto' }); lastChapterScrollMetricsRef.current = null;
setChapterScrollTop(scrollRef.current, 0);
pendingChapterRestoreRatioRef.current = null; pendingChapterRestoreRatioRef.current = null;
const params = new URLSearchParams({ const params = new URLSearchParams({
sourceId: manifest.book.sourceId, sourceId: manifest.book.sourceId,
@@ -930,18 +1019,21 @@ function ChapterReader({ manifest }: { manifest: BookReadManifest }) {
const node = scrollRef.current; const node = scrollRef.current;
if (!node) return; if (!node) return;
if (settings.mode === 'scrolled') return; if (settings.mode === 'scrolled') return;
const delta = Math.max(240, node.clientHeight * 0.88) * direction; const metrics = getChapterScrollMetrics(node);
const maxTop = Math.max(0, node.scrollHeight - node.clientHeight); const delta = Math.max(240, metrics.clientHeight * 0.88) * direction;
const nextTop = Math.max(0, Math.min(maxTop, node.scrollTop + delta)); const nextTop = Math.max(
if (direction > 0 && node.scrollTop >= maxTop - 8) { 0,
Math.min(metrics.maxScrollTop, metrics.scrollTop + delta)
);
if (direction > 0 && metrics.scrollTop >= metrics.maxScrollTop - 8) {
if (currentIndex < chapters.length - 1) goNextChapter(); if (currentIndex < chapters.length - 1) goNextChapter();
return; return;
} }
if (direction < 0 && node.scrollTop <= 8) { if (direction < 0 && metrics.scrollTop <= 8) {
if (currentIndex > 0) goPrevChapter(); if (currentIndex > 0) goPrevChapter();
return; return;
} }
node.scrollTo({ top: nextTop, behavior: 'smooth' }); setChapterScrollTop(node, nextTop);
}, },
[chapters.length, currentIndex, goNextChapter, goPrevChapter, settings.mode] [chapters.length, currentIndex, goNextChapter, goPrevChapter, settings.mode]
); );
@@ -1062,12 +1154,12 @@ function ChapterReader({ manifest }: { manifest: BookReadManifest }) {
if (attempt < 20) window.setTimeout(() => restore(attempt + 1), 100); if (attempt < 20) window.setTimeout(() => restore(attempt + 1), 100);
return; return;
} }
const maxScrollTop = Math.max(0, node.scrollHeight - node.clientHeight); const { maxScrollTop } = getChapterScrollMetrics(node);
if (maxScrollTop <= 0 && attempt < 20) { if (maxScrollTop <= 0 && attempt < 20) {
window.setTimeout(() => restore(attempt + 1), 100); window.setTimeout(() => restore(attempt + 1), 100);
return; return;
} }
node.scrollTo({ top: maxScrollTop * savedRatio, behavior: 'auto' }); setChapterScrollTop(node, maxScrollTop * savedRatio);
restoredChapterPositionRef.current = true; restoredChapterPositionRef.current = true;
pendingChapterRestoreRatioRef.current = null; pendingChapterRestoreRatioRef.current = null;
window.setTimeout(() => persistChapterProgress(), 0); window.setTimeout(() => persistChapterProgress(), 0);
@@ -1082,11 +1174,21 @@ function ChapterReader({ manifest }: { manifest: BookReadManifest }) {
useEffect(() => { useEffect(() => {
const node = scrollRef.current; const node = scrollRef.current;
if (!node) return; if (!node) return;
node.addEventListener('scroll', scheduleChapterProgressSave, { const handleScroll = () => {
lastChapterScrollMetricsRef.current = getChapterScrollMetrics(
scrollRef.current
);
scheduleChapterProgressSave();
};
node.addEventListener('scroll', handleScroll, {
passive: true,
});
window.addEventListener('scroll', handleScroll, {
passive: true, passive: true,
}); });
return () => { return () => {
node.removeEventListener('scroll', scheduleChapterProgressSave); node.removeEventListener('scroll', handleScroll);
window.removeEventListener('scroll', handleScroll);
if (chapterSaveTimerRef.current) { if (chapterSaveTimerRef.current) {
window.clearTimeout(chapterSaveTimerRef.current); window.clearTimeout(chapterSaveTimerRef.current);
chapterSaveTimerRef.current = null; chapterSaveTimerRef.current = null;
@@ -2217,8 +2319,37 @@ export default function BookReadPage() {
clientHeight: metrics.clientHeight, clientHeight: metrics.clientHeight,
updatedAt: Date.now(), updatedAt: Date.now(),
}); });
const chapterTitle = lastChapterRef.current || currentChapter || manifest.book.title;
pendingRecordRef.current = {
sourceId: manifest.book.sourceId,
sourceName: manifest.book.sourceName,
bookId: manifest.book.id,
title: manifest.book.title,
author: manifest.book.author,
cover: manifest.book.cover,
detailHref: manifest.book.detailHref,
acquisitionHref: manifest.acquisitionHref,
format: manifest.format,
locator: {
type: 'href',
value: encodeChapterScrollLocator(
href,
metrics.scrollTop,
metrics.scrollHeight,
metrics.clientHeight
),
href,
chapterTitle,
},
chapterTitle,
chapterHref: href,
progressPercent:
lastProgressRef.current ?? manifest.lastRecord?.progressPercent ?? 0,
saveTime: Date.now(),
};
pendingRecordDirtyRef.current = true;
}, },
[manifest] [currentChapter, manifest]
); );
const applyPendingScrolledRestore = useCallback(() => { const applyPendingScrolledRestore = useCallback(() => {
@@ -2285,6 +2416,27 @@ export default function BookReadPage() {
await renditionRef.current.display(target); await renditionRef.current.display(target);
}, []); }, []);
const captureCurrentEpubLocation = useCallback(() => {
const location = renditionRef.current?.currentLocation?.();
if (location?.start?.cfi || location?.end?.cfi) {
lastLocationRef.current = location;
currentHrefRef.current =
location.start?.href || currentHrefRef.current || '';
return location;
}
return lastLocationRef.current;
}, []);
const switchReaderMode = useCallback(
(mode: ReaderMode) => {
if (mode === settingsRef.current.mode) return;
captureCurrentEpubLocation();
persistCurrentProgress();
setSettings((prev) => ({ ...prev, mode }));
},
[captureCurrentEpubLocation, persistCurrentProgress]
);
const handleReaderTap = useCallback( const handleReaderTap = useCallback(
(zone: 'left' | 'center' | 'right') => { (zone: 'left' | 'center' | 'right') => {
if (!ready) return; if (!ready) return;
@@ -2619,7 +2771,11 @@ export default function BookReadPage() {
currentSessionHref || currentSessionHref ||
manifest.lastRecord?.chapterHref || manifest.lastRecord?.chapterHref ||
manifest.lastRecord?.locator?.href || manifest.lastRecord?.locator?.href ||
getHrefFromScrollLocator(manifest.lastRecord?.locator?.value) ||
undefined; undefined;
const historyScrolledPosition = getScrolledPositionFromRecord(
manifest.lastRecord
);
const cachedScrolledPosition = initialScrolledHref const cachedScrolledPosition = initialScrolledHref
? getScrolledPosition( ? getScrolledPosition(
manifest.book.sourceId, manifest.book.sourceId,
@@ -2628,12 +2784,16 @@ export default function BookReadPage() {
) )
: null; : null;
pendingScrolledRestoreRef.current = pendingScrolledRestoreRef.current =
settings.mode === 'scrolled' && !currentSessionHref settings.mode === 'scrolled' && !currentSessionCfi
? cachedScrolledPosition ? historyScrolledPosition || cachedScrolledPosition
: null; : null;
restoreTargetRef.current = restoreTargetRef.current =
settings.mode === 'scrolled' settings.mode === 'scrolled'
? initialScrolledHref || cachedScrolledPosition?.href || undefined ? currentSessionCfi ||
initialScrolledHref ||
historyScrolledPosition?.href ||
cachedScrolledPosition?.href ||
undefined
: currentSessionCfi || manifest.lastRecord?.locator?.value || undefined; : currentSessionCfi || manifest.lastRecord?.locator?.value || undefined;
loadEpubScript() loadEpubScript()
@@ -3422,9 +3582,7 @@ export default function BookReadPage() {
).map((mode) => ( ).map((mode) => (
<button <button
key={mode.key} key={mode.key}
onClick={() => onClick={() => switchReaderMode(mode.key)}
setSettings((prev) => ({ ...prev, mode: mode.key }))
}
className={`rounded-2xl border px-3 py-3 text-left ${ className={`rounded-2xl border px-3 py-3 text-left ${
settings.mode === mode.key settings.mode === mode.key
? 'border-emerald-500 bg-emerald-50 text-emerald-700 dark:bg-emerald-950/30 dark:text-emerald-300' ? 'border-emerald-500 bg-emerald-50 text-emerald-700 dark:bg-emerald-950/30 dark:text-emerald-300'