预加载和适配屏幕修复
This commit is contained in:
@@ -7,6 +7,7 @@ import { ArrowDownWideNarrow, ArrowUpWideNarrow } from 'lucide-react';
|
|||||||
|
|
||||||
import { getAllMangaReadRecords, saveMangaReadRecord } from '@/lib/db.client';
|
import { getAllMangaReadRecords, saveMangaReadRecord } from '@/lib/db.client';
|
||||||
import type { MangaChapter, MangaDetail, MangaReadRecord } from '@/lib/manga.types';
|
import type { MangaChapter, MangaDetail, MangaReadRecord } from '@/lib/manga.types';
|
||||||
|
import { processImageUrl } from '@/lib/utils';
|
||||||
|
|
||||||
import ProxyImage from '@/components/ProxyImage';
|
import ProxyImage from '@/components/ProxyImage';
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ const READ_MODE_STORAGE_KEY = 'mangaReadMode';
|
|||||||
const SCALE_MODE_STORAGE_KEY = 'mangaScaleMode';
|
const SCALE_MODE_STORAGE_KEY = 'mangaScaleMode';
|
||||||
const PAGE_GAP_STORAGE_KEY = 'mangaPageGap';
|
const PAGE_GAP_STORAGE_KEY = 'mangaPageGap';
|
||||||
const SAVE_INTERVAL_MS = 10000;
|
const SAVE_INTERVAL_MS = 10000;
|
||||||
|
const PRELOAD_PAGE_COUNT = 5;
|
||||||
|
|
||||||
const READ_MODE_OPTIONS: Array<{ value: ReadMode; label: string }> = [
|
const READ_MODE_OPTIONS: Array<{ value: ReadMode; label: string }> = [
|
||||||
{ value: 'single', label: '单页' },
|
{ value: 'single', label: '单页' },
|
||||||
@@ -98,6 +100,7 @@ export default function MangaReadPage() {
|
|||||||
const previousReadModeRef = useRef<ReadMode>('vertical');
|
const previousReadModeRef = useRef<ReadMode>('vertical');
|
||||||
const requestVerticalPageSyncRef = useRef<(() => void) | null>(null);
|
const requestVerticalPageSyncRef = useRef<(() => void) | null>(null);
|
||||||
const currentVerticalPageIndexRef = useRef(0);
|
const currentVerticalPageIndexRef = useRef(0);
|
||||||
|
const preloadedImageUrlsRef = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
const getCurrentVerticalPageIndex = () => {
|
const getCurrentVerticalPageIndex = () => {
|
||||||
if (!verticalPageRefs.current.length) return 0;
|
if (!verticalPageRefs.current.length) return 0;
|
||||||
@@ -121,6 +124,17 @@ export default function MangaReadPage() {
|
|||||||
requestVerticalPageSyncRef.current?.();
|
requestVerticalPageSyncRef.current?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPreloadAnchorPage = () => (
|
||||||
|
readMode === 'vertical' ? currentVerticalPageIndexRef.current : activePage
|
||||||
|
);
|
||||||
|
|
||||||
|
const getImageLoadingStrategy = (index: number): 'eager' | 'lazy' => {
|
||||||
|
const anchorPage = getPreloadAnchorPage();
|
||||||
|
return index >= Math.max(anchorPage - 1, 0) && index <= anchorPage + PRELOAD_PAGE_COUNT
|
||||||
|
? 'eager'
|
||||||
|
: 'lazy';
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
const savedMode = window.localStorage.getItem(READ_MODE_STORAGE_KEY) as ReadMode | null;
|
const savedMode = window.localStorage.getItem(READ_MODE_STORAGE_KEY) as ReadMode | null;
|
||||||
@@ -206,6 +220,7 @@ export default function MangaReadPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActivePage(0);
|
setActivePage(0);
|
||||||
restoredChapterKeyRef.current = null;
|
restoredChapterKeyRef.current = null;
|
||||||
|
preloadedImageUrlsRef.current.clear();
|
||||||
}, [chapterId]);
|
}, [chapterId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -393,6 +408,23 @@ export default function MangaReadPage() {
|
|||||||
pendingRecordDirtyRef.current = true;
|
pendingRecordDirtyRef.current = true;
|
||||||
}, [activePage, chapterId, chapterName, cover, mangaId, pages.length, readMode, sourceId, sourceName, title]);
|
}, [activePage, chapterId, chapterName, cover, mangaId, pages.length, readMode, sourceId, sourceName, title]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined' || !pages.length) return;
|
||||||
|
|
||||||
|
const anchorPage = getPreloadAnchorPage();
|
||||||
|
const preloadTargets = pages.slice(anchorPage + 1, anchorPage + 1 + PRELOAD_PAGE_COUNT);
|
||||||
|
|
||||||
|
preloadTargets.forEach((page) => {
|
||||||
|
const resolvedUrl = processImageUrl(page);
|
||||||
|
if (!resolvedUrl || preloadedImageUrlsRef.current.has(resolvedUrl)) return;
|
||||||
|
|
||||||
|
const img = new window.Image();
|
||||||
|
img.decoding = 'async';
|
||||||
|
img.src = resolvedUrl;
|
||||||
|
preloadedImageUrlsRef.current.add(resolvedUrl);
|
||||||
|
});
|
||||||
|
}, [activePage, pages, readMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mangaId || !sourceId || !chapterId) return;
|
if (!mangaId || !sourceId || !chapterId) return;
|
||||||
|
|
||||||
@@ -576,12 +608,12 @@ export default function MangaReadPage() {
|
|||||||
|
|
||||||
const imageClassName = useMemo(() => {
|
const imageClassName = useMemo(() => {
|
||||||
if (scaleMode === 'original') {
|
if (scaleMode === 'original') {
|
||||||
return 'mx-auto h-auto w-auto max-w-none object-none';
|
return 'block mx-auto h-auto w-auto max-w-none object-none';
|
||||||
}
|
}
|
||||||
if (readMode === 'single' || readMode === 'double') {
|
if (readMode === 'single' || readMode === 'double') {
|
||||||
return 'mx-auto max-h-[calc(100vh-8rem)] w-auto max-w-full object-contain';
|
return 'block h-auto w-full object-contain sm:mx-auto sm:max-h-[calc(100vh-8rem)] sm:w-auto sm:max-w-full';
|
||||||
}
|
}
|
||||||
return 'h-auto w-full object-contain';
|
return 'block h-auto w-full object-contain';
|
||||||
}, [readMode, scaleMode]);
|
}, [readMode, scaleMode]);
|
||||||
|
|
||||||
const handleReaderClick = (event: MouseEvent<HTMLDivElement>) => {
|
const handleReaderClick = (event: MouseEvent<HTMLDivElement>) => {
|
||||||
@@ -742,7 +774,7 @@ export default function MangaReadPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className='relative min-h-[calc(100vh-5rem)] select-none px-2 py-3 sm:px-3'
|
className='relative min-h-[calc(100vh-5rem)] select-none px-0 py-3 sm:px-3'
|
||||||
onClick={handleReaderClick}
|
onClick={handleReaderClick}
|
||||||
>
|
>
|
||||||
{showChapterComplete && (
|
{showChapterComplete && (
|
||||||
@@ -812,6 +844,7 @@ export default function MangaReadPage() {
|
|||||||
originalSrc={page}
|
originalSrc={page}
|
||||||
alt={`${chapterName}-${index + 1}`}
|
alt={`${chapterName}-${index + 1}`}
|
||||||
className={imageClassName}
|
className={imageClassName}
|
||||||
|
loading={getImageLoadingStrategy(index)}
|
||||||
onLoad={handleVerticalImageLoad}
|
onLoad={handleVerticalImageLoad}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -824,9 +857,14 @@ export default function MangaReadPage() {
|
|||||||
style={{ gap: `${pageGap}px` }}
|
style={{ gap: `${pageGap}px` }}
|
||||||
>
|
>
|
||||||
{pages.map((page, index) => (
|
{pages.map((page, index) => (
|
||||||
<div key={`${page}-${index}`} className='flex min-w-full snap-center items-center justify-center px-1'>
|
<div key={`${page}-${index}`} className='flex min-w-full snap-center items-center justify-center px-1'>
|
||||||
<div className='w-full overflow-hidden bg-gray-100 shadow-sm dark:bg-gray-900'>
|
<div className='w-full overflow-hidden bg-gray-100 shadow-sm dark:bg-gray-900'>
|
||||||
<ProxyImage originalSrc={page} alt={`${chapterName}-${index + 1}`} className={imageClassName} />
|
<ProxyImage
|
||||||
|
originalSrc={page}
|
||||||
|
alt={`${chapterName}-${index + 1}`}
|
||||||
|
className={imageClassName}
|
||||||
|
loading={getImageLoadingStrategy(index)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -843,6 +881,7 @@ export default function MangaReadPage() {
|
|||||||
originalSrc={page}
|
originalSrc={page}
|
||||||
alt={`${chapterName}-${activePage + index + 1}`}
|
alt={`${chapterName}-${activePage + index + 1}`}
|
||||||
className={imageClassName}
|
className={imageClassName}
|
||||||
|
loading='eager'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ const ProxyImage: React.FC<ProxyImageProps> = ({
|
|||||||
displaySrc,
|
displaySrc,
|
||||||
retryDelay = 2000,
|
retryDelay = 2000,
|
||||||
retryOnError = true,
|
retryOnError = true,
|
||||||
|
loading = 'lazy',
|
||||||
|
decoding = 'async',
|
||||||
onError,
|
onError,
|
||||||
src: _src,
|
src: _src,
|
||||||
...props
|
...props
|
||||||
@@ -41,6 +43,8 @@ const ProxyImage: React.FC<ProxyImageProps> = ({
|
|||||||
<img
|
<img
|
||||||
{...props}
|
{...props}
|
||||||
src={displaySrc || processImageUrl(originalSrc)}
|
src={displaySrc || processImageUrl(originalSrc)}
|
||||||
|
loading={loading}
|
||||||
|
decoding={decoding}
|
||||||
onError={handleError}
|
onError={handleError}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -151,8 +151,10 @@ export default function MangaLayout({ children }: MangaLayoutProps) {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main
|
<main
|
||||||
className={`mx-auto max-w-7xl px-3 pt-20 sm:px-6 sm:pt-24 ${
|
className={`mx-auto max-w-7xl pt-20 sm:pt-24 ${
|
||||||
isReadingPage ? 'pb-24 sm:pb-28 lg:pb-10' : 'pb-[calc(5rem+env(safe-area-inset-bottom))] sm:pb-[calc(5rem+env(safe-area-inset-bottom))] lg:pb-10'
|
isReadingPage
|
||||||
|
? 'px-0 pb-24 sm:px-0 sm:pb-28 lg:pb-10'
|
||||||
|
: 'px-3 pb-[calc(5rem+env(safe-area-inset-bottom))] sm:px-6 sm:pb-[calc(5rem+env(safe-area-inset-bottom))] lg:pb-10'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
Reference in New Issue
Block a user