修复因dc31a788fc7ee8b23f07f1cfdcd5748c26103c24 产生的bug
This commit is contained in:
+181
-23
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user