From 174d850093b896462111af3ee4279199e568d0b4 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Mon, 10 Feb 2025 01:05:35 +0100 Subject: [PATCH] Preload previous/next chapter in reader --- .../components/viewer/ReaderChapterViewer.tsx | 66 +++++++++++-------- .../reader/components/viewer/ReaderViewer.tsx | 23 ++++++- .../components/viewer/pager/BasePager.tsx | 2 +- .../viewer/pager/ReaderDoublePagedPager.tsx | 4 +- .../viewer/pager/ReaderHorizontalPager.tsx | 5 +- .../viewer/pager/ReaderPagedPager.tsx | 3 +- .../viewer/pager/ReaderVerticalPager.tsx | 5 +- .../state/ReaderStateChaptersContext.tsx | 2 + .../reader/hooks/useReaderSetChaptersState.ts | 2 + src/modules/reader/services/ReaderControls.ts | 38 ++++++++++- src/modules/reader/services/ReaderService.ts | 39 +++++++++++ src/modules/reader/types/Reader.types.ts | 3 + src/modules/reader/utils/Reader.utils.ts | 31 ++++++++- .../reader/utils/ReaderPager.utils.tsx | 6 +- 14 files changed, 185 insertions(+), 44 deletions(-) diff --git a/src/modules/reader/components/viewer/ReaderChapterViewer.tsx b/src/modules/reader/components/viewer/ReaderChapterViewer.tsx index 826bcbd9..e87163e1 100644 --- a/src/modules/reader/components/viewer/ReaderChapterViewer.tsx +++ b/src/modules/reader/components/viewer/ReaderChapterViewer.tsx @@ -78,6 +78,7 @@ const BaseReaderChapterViewer = ({ isNextChapter, isLeadingChapter, isTrailingChapter, + isPreloadMode, imageRefs: globalImageRefs, scrollIntoView, setReaderStateChapters, @@ -172,7 +173,15 @@ const BaseReaderChapterViewer = ({ const Pager = useMemo(() => getPagerForReadingMode(readingMode), [readingMode]); const isLtrReadingDirection = readingDirection === ReadingDirection.LTR; const isContinuousReadingModeActive = isContinuousReadingMode(readingMode); - const shouldHideChapter = !isContinuousReadingModeActive && !isCurrentChapter; + const shouldHideChapter = (!isContinuousReadingModeActive && !isCurrentChapter) || isPreloadMode; + + const isCurrentChapterInSinglePager = !isContinuousReadingModeActive && isCurrentChapter; + const showPreviousTransitionPage = + !shouldHideChapter && + (isCurrentChapterInSinglePager || (isContinuousReadingModeActive && (isInitialChapter || isLeadingChapter))); + const showNextTransitionPage = + !shouldHideChapter && + (isCurrentChapterInSinglePager || (isContinuousReadingModeActive && (isInitialChapter || isTrailingChapter))); isCurrentChapterRef.current = isCurrentChapter; if (isCurrentChapter) { @@ -240,7 +249,7 @@ const BaseReaderChapterViewer = ({ ), ); - const updatePageState = ( + const updateState = ( value: T, setLocalState: (value: T) => void, setGlobalState: (value: T) => void, @@ -264,13 +273,13 @@ const BaseReaderChapterViewer = ({ pagesToSpreadState, arePagesFetched, setArePagesFetched, - setReaderStateChapters, - (value) => updatePageState(value, setTotalPages, setContextTotalPages), - (value) => updatePageState(value, setPages, setContextPages), - (value) => updatePageState(value, setPageUrls, noOp), - (value) => updatePageState(value, setPageLoadStates, setContextPageLoadStates), - (value) => updatePageState(value, setPagesToSpreadState, noOp), - (value) => updatePageState(value, noOp, setContextCurrentPageIndex), + (value) => updateState(value, noOp, setReaderStateChapters), + (value) => updateState(value, setTotalPages, setContextTotalPages), + (value) => updateState(value, setPages, setContextPages), + (value) => updateState(value, setPageUrls, noOp), + (value) => updateState(value, setPageLoadStates, setContextPageLoadStates), + (value) => updateState(value, setPagesToSpreadState, noOp), + (value) => updateState(value, noOp, setContextCurrentPageIndex), (value) => { if ((isInitialChapter && !arePagesFetched) || scrollIntoView) { setPageToScrollToIndex(value); @@ -280,16 +289,16 @@ const BaseReaderChapterViewer = ({ })); } }, - (value) => updatePageState(value, noOp, setTransitionPageMode), + (value) => updateState(value, noOp, setTransitionPageMode), ); useReaderConvertPagesForReadingMode( currentPageIndex, actualPages, pageUrls, - (value) => updatePageState(value, setPages, setContextPages, true), - (value) => updatePageState(value, setPagesToSpreadState, noOp, true), - (value) => updatePageState(value, noOp, updateCurrentPageIndex), + (value) => updateState(value, setPages, setContextPages, true), + (value) => updateState(value, setPagesToSpreadState, noOp, true), + (value) => updateState(value, noOp, updateCurrentPageIndex), readingMode, ); @@ -396,19 +405,20 @@ const BaseReaderChapterViewer = ({ }), }} > - - {((!isContinuousReadingModeActive && isCurrentChapter) || - (isContinuousReadingModeActive && (isInitialChapter || isLeadingChapter))) && ( + {!isPreloadMode && ( + + )} + {showPreviousTransitionPage && ( )} - {((!isContinuousReadingModeActive && isCurrentChapter) || - (isContinuousReadingModeActive && (isInitialChapter || isTrailingChapter))) && ( + {showNextTransitionPage && ( )} diff --git a/src/modules/reader/components/viewer/ReaderViewer.tsx b/src/modules/reader/components/viewer/ReaderViewer.tsx index 2d5a4cd3..48e57056 100644 --- a/src/modules/reader/components/viewer/ReaderViewer.tsx +++ b/src/modules/reader/components/viewer/ReaderViewer.tsx @@ -314,6 +314,24 @@ const BaseReaderViewer = forwardRef( const isNextChapter = chapter.id === chaptersToRender[currentChapterIndex - 1]?.id; const isLeadingChapter = initialChapter.sourceOrder > chapter.sourceOrder; const isTrailingChapter = initialChapter.sourceOrder < chapter.sourceOrder; + const isLastLeadingChapter = visibleChapters.lastLeadingChapterSourceOrder === chapter.sourceOrder; + const isLastTrailingChapter = + visibleChapters.lastTrailingChapterSourceOrder === chapter.sourceOrder; + const isPreloadMode = + (isLastLeadingChapter && visibleChapters.isLeadingChapterPreloadMode) || + (isLastTrailingChapter && visibleChapters.isTrailingChapterPreloadMode); + + const isPreviousChapterLoaded = !!chaptersToRender[currentChapterIndex + 1]; + const isPreviousChapterLastLeadingChapter = chapterIndex + 1 >= chaptersToRender.length - 1; + const isPreviousChapterPreloading = + isPreviousChapterLastLeadingChapter && visibleChapters.isLeadingChapterPreloadMode; + const isPreviousChapterVisible = isPreviousChapterLoaded && !isPreviousChapterPreloading; + + const isNextChapterLoaded = !!chaptersToRender[chapterIndex - 1]; + const isNextChapterLastTrailingChapter = chapterIndex - 1 < 0; + const isNextChapterPreloading = + isNextChapterLastTrailingChapter && visibleChapters.isTrailingChapterPreloadMode; + const isNextChapterVisible = isNextChapterLoaded && !isNextChapterPreloading; return ( & + Omit & Pick & { createPage: ( page: ReaderStatePages['pages'][number], diff --git a/src/modules/reader/components/viewer/pager/ReaderDoublePagedPager.tsx b/src/modules/reader/components/viewer/pager/ReaderDoublePagedPager.tsx index 6d79c3b7..e40e35ea 100644 --- a/src/modules/reader/components/viewer/pager/ReaderDoublePagedPager.tsx +++ b/src/modules/reader/components/viewer/pager/ReaderDoublePagedPager.tsx @@ -44,7 +44,7 @@ const getPagePosition = ( const BaseReaderDoublePagedPager = forwardRef< HTMLDivElement, ReaderPagerProps & Pick ->(({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, ...props }, ref) => { +>(({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => { const { currentPageIndex, pages, totalPages, readingDirection, pageScaleMode } = props; const { direction: themeDirection } = useTheme(); @@ -72,6 +72,7 @@ const BaseReaderDoublePagedPager = forwardRef< pagesIndex, true, pageLoadStates[primary.index].loaded, + isPreloadMode, onLoad, onError, shouldLoad, @@ -89,6 +90,7 @@ const BaseReaderDoublePagedPager = forwardRef< pagesIndex, false, pageLoadStates[secondary.index].loaded, + isPreloadMode, onLoad, onError, shouldLoad, diff --git a/src/modules/reader/components/viewer/pager/ReaderHorizontalPager.tsx b/src/modules/reader/components/viewer/pager/ReaderHorizontalPager.tsx index 0e32b7e5..38b28835 100644 --- a/src/modules/reader/components/viewer/pager/ReaderHorizontalPager.tsx +++ b/src/modules/reader/components/viewer/pager/ReaderHorizontalPager.tsx @@ -16,7 +16,7 @@ import { createReaderPage } from '@/modules/reader/utils/ReaderPager.utils.tsx'; const BaseReaderHorizontalPager = forwardRef< HTMLDivElement, ReaderPagerProps & Pick ->(({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, ...props }, ref) => { +>(({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => { const { currentPageIndex, totalPages, pageGap, readingDirection } = props; const { direction: themeDirection } = useTheme(); @@ -33,10 +33,11 @@ const BaseReaderHorizontalPager = forwardRef< pagesIndex, true, pageLoadStates[page.primary.index].loaded, + isPreloadMode, onLoad, onError, shouldLoad, - true, + !isPreloadMode, currentPageIndex, totalPages, ...baseProps, diff --git a/src/modules/reader/components/viewer/pager/ReaderPagedPager.tsx b/src/modules/reader/components/viewer/pager/ReaderPagedPager.tsx index 23ab4194..66e5a374 100644 --- a/src/modules/reader/components/viewer/pager/ReaderPagedPager.tsx +++ b/src/modules/reader/components/viewer/pager/ReaderPagedPager.tsx @@ -12,7 +12,7 @@ import { ReaderPagerProps } from '@/modules/reader/types/Reader.types.ts'; import { createReaderPage } from '@/modules/reader/utils/ReaderPager.utils.tsx'; const BaseReaderPagedPager = forwardRef( - ({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, ...props }, ref) => { + ({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => { const { currentPageIndex, totalPages } = props; return ( @@ -25,6 +25,7 @@ const BaseReaderPagedPager = forwardRef( pagesIndex, true, pageLoadStates[page.primary.index].loaded, + isPreloadMode, onLoad, onError, shouldLoad, diff --git a/src/modules/reader/components/viewer/pager/ReaderVerticalPager.tsx b/src/modules/reader/components/viewer/pager/ReaderVerticalPager.tsx index 31c292d5..9f857c8e 100644 --- a/src/modules/reader/components/viewer/pager/ReaderVerticalPager.tsx +++ b/src/modules/reader/components/viewer/pager/ReaderVerticalPager.tsx @@ -12,7 +12,7 @@ import { ReaderPagerProps, ReadingMode } from '@/modules/reader/types/Reader.typ import { createReaderPage } from '@/modules/reader/utils/ReaderPager.utils.tsx'; const BaseReaderVerticalPager = forwardRef( - ({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, ...props }, ref) => { + ({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => { const { currentPageIndex, totalPages, readingMode, pageGap } = props; const isWebtoonMode = readingMode === ReadingMode.WEBTOON; @@ -28,10 +28,11 @@ const BaseReaderVerticalPager = forwardRef( pagesIndex, true, pageLoadStates[page.primary.index].loaded, + isPreloadMode, onLoad, onError, shouldLoad, - true, + !isPreloadMode, currentPageIndex, totalPages, ...baseProps, diff --git a/src/modules/reader/contexts/state/ReaderStateChaptersContext.tsx b/src/modules/reader/contexts/state/ReaderStateChaptersContext.tsx index 582b688a..f10a09c4 100644 --- a/src/modules/reader/contexts/state/ReaderStateChaptersContext.tsx +++ b/src/modules/reader/contexts/state/ReaderStateChaptersContext.tsx @@ -18,6 +18,8 @@ export const READER_STATE_CHAPTERS_DEFAULTS: Omit void { const { currentPageIndex, setCurrentPageIndex } = userReaderStatePagesContext(); - const { chapterForDuplicatesHandling, currentChapter, nextChapter, mangaChapters } = - useReaderStateChaptersContext(); + const { + chapterForDuplicatesHandling, + currentChapter, + previousChapter, + nextChapter, + mangaChapters, + visibleChapters, + setReaderStateChapters, + } = useReaderStateChaptersContext(); const updateChapter = ReaderService.useUpdateChapter(); const { shouldSkipDupChapters } = ReaderService.useSettings(); const { @@ -470,13 +479,28 @@ export class ReaderControls { return useCallback( (pageIndex, debounceChapterUpdate = true, endReached = false) => { + if (pageIndex === currentPageIndex) { + return; + } + setCurrentPageIndex(pageIndex); if (!currentChapter) { return; } + const direction = currentPageIndex > pageIndex ? DirectionOffset.PREVIOUS : DirectionOffset.NEXT; + ReaderService.downloadAhead(currentChapter, nextChapter, nextChapters, pageIndex, downloadAheadLimit); + ReaderService.preloadChapter( + pageIndex, + currentChapter.pageCount, + direction === DirectionOffset.NEXT ? nextChapter : previousChapter, + visibleChapters.lastLeadingChapterSourceOrder, + visibleChapters.lastTrailingChapterSourceOrder, + setReaderStateChapters, + direction, + ); const handleCurrentPageIndexChange = () => { const currentChapterUpToDate = getReaderChapterFromCache(currentChapter.id); @@ -501,7 +525,15 @@ export class ReaderControls { handleCurrentPageIndexChange(); }, - [currentChapter?.id, nextChapter?.id, nextChapters, currentPageIndex, downloadAheadLimit], + [ + currentChapter?.id, + previousChapter?.id, + nextChapter?.id, + nextChapters, + currentPageIndex, + downloadAheadLimit, + visibleChapters, + ], ); } diff --git a/src/modules/reader/services/ReaderService.ts b/src/modules/reader/services/ReaderService.ts index adb5e6bc..afe436d0 100644 --- a/src/modules/reader/services/ReaderService.ts +++ b/src/modules/reader/services/ReaderService.ts @@ -18,6 +18,7 @@ import { ReaderExitMode, ReaderOverlayMode, ReaderResumeMode, + ReaderStateChapters, ReadingDirection, ReadingMode, } from '@/modules/reader/types/Reader.types.ts'; @@ -44,6 +45,8 @@ import { getChapterIdsForDownloadAhead, getChapterIdsToDeleteForChapterUpdate, getReaderChapterFromCache, + isInDownloadAheadRange, + updateReaderStateVisibleChapters, } from '@/modules/reader/utils/Reader.utils.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { Queue } from '@/lib/Queue.ts'; @@ -118,6 +121,42 @@ export class ReaderService { }); } + static preloadChapter( + pageIndex: number, + pageCount: number, + chapter: TChapterReader | undefined | null, + lastLeadingChapterSourceOrder: number, + lastTrailingChapterSourceOrder: number, + setReaderStateChapters: ReaderStateChapters['setReaderStateChapters'], + direction: DirectionOffset, + ): void { + if (!chapter) { + return; + } + + const isPreviousChapter = direction === DirectionOffset.PREVIOUS; + const isNextChapter = direction === DirectionOffset.NEXT; + + const isAlreadyPreloaded = + (isPreviousChapter && lastLeadingChapterSourceOrder <= chapter.sourceOrder) || + (isNextChapter && lastTrailingChapterSourceOrder >= chapter.sourceOrder); + const shouldPreload = !isAlreadyPreloaded && isInDownloadAheadRange(pageIndex, pageCount, direction); + if (!shouldPreload) { + return; + } + + setReaderStateChapters((state) => + updateReaderStateVisibleChapters( + isPreviousChapter, + state, + chapter.sourceOrder, + false, + isPreviousChapter ? true : undefined, + isNextChapter ? true : undefined, + ), + ); + } + static useUpdateChapter(): (patch: UpdateChapterPatchInput) => void { const { manga } = useReaderStateMangaContext(); const { chapterForDuplicatesHandling, currentChapter, mangaChapters } = useReaderStateChaptersContext(); diff --git a/src/modules/reader/types/Reader.types.ts b/src/modules/reader/types/Reader.types.ts index 1577a105..cf8f56b9 100644 --- a/src/modules/reader/types/Reader.types.ts +++ b/src/modules/reader/types/Reader.types.ts @@ -213,6 +213,8 @@ export interface ReaderStateChapters { trailing: number; lastLeadingChapterSourceOrder: number; lastTrailingChapterSourceOrder: number; + isLeadingChapterPreloadMode: boolean; + isTrailingChapterPreloadMode: boolean; scrollIntoView: boolean; resumeMode?: ReaderResumeMode; }; @@ -289,6 +291,7 @@ export interface ReaderPagerProps isCurrentChapter: boolean; isPreviousChapter: boolean; isNextChapter: boolean; + isPreloadMode: boolean; } export enum PageInViewportType { diff --git a/src/modules/reader/utils/Reader.utils.ts b/src/modules/reader/utils/Reader.utils.ts index d89e58f0..b0084bfe 100644 --- a/src/modules/reader/utils/Reader.utils.ts +++ b/src/modules/reader/utils/Reader.utils.ts @@ -19,6 +19,7 @@ import { CHAPTER_READER_FIELDS } from '@/lib/graphql/fragments/ChapterFragments. import { isPageOfOutdatedPageLoadStates, isSpreadPage } from '@/modules/reader/utils/ReaderPager.utils.tsx'; import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts'; import { coerceIn } from '@/lib/HelperFunctions.ts'; +import { DirectionOffset } from '@/Base.types.ts'; export const getInitialReaderPageIndex = ( resumeMode: ReaderResumeMode, @@ -76,6 +77,20 @@ export const getChapterIdsToDeleteForChapterUpdate = ( return Chapters.getIds(Chapters.addDuplicates([chapterToDelete], chapters)); }; +export const isInDownloadAheadRange = ( + currentPageIndex: number, + pageCount: number, + direction: DirectionOffset = DirectionOffset.NEXT, +): boolean => { + const progress = (currentPageIndex + 1) / pageCount; + + if (direction === DirectionOffset.PREVIOUS) { + return progress < 0.75; + } + + return progress > 0.25; +}; + export const getChapterIdsForDownloadAhead = ( chapter: TChapterReader, nextChapter: TChapterReader | undefined, @@ -89,8 +104,10 @@ export const getChapterIdsForDownloadAhead = ( } const isDownloadAheadEnabled = !!downloadAheadLimit; - const inDownloadRange = (currentPageIndex + 1) / chapterUpToDateData.pageCount > 0.25; - const shouldCheckDownloadAhead = isDownloadAheadEnabled && chapterUpToDateData.isDownloaded && inDownloadRange; + const shouldCheckDownloadAhead = + isDownloadAheadEnabled && + chapterUpToDateData.isDownloaded && + isInDownloadAheadRange(currentPageIndex, chapterUpToDateData.pageCount); if (!shouldCheckDownloadAhead) { return []; } @@ -208,6 +225,8 @@ export const updateReaderStateVisibleChapters = ( state: Omit, chapterToOpenSourceOrder: TChapterReader['sourceOrder'], scrollIntoView: boolean, + isLeadingChapterPreloadMode?: boolean, + isTrailingChapterPreloadMode?: boolean, ): Omit => { const { leading, trailing, lastLeadingChapterSourceOrder, lastTrailingChapterSourceOrder } = state.visibleChapters; @@ -226,6 +245,14 @@ export const updateReaderStateVisibleChapters = ( lastTrailingChapterSourceOrder: isNewTrailingChapter ? chapterToOpenSourceOrder : lastTrailingChapterSourceOrder, + isLeadingChapterPreloadMode: + isLeadingChapterPreloadMode !== undefined + ? isLeadingChapterPreloadMode + : state.visibleChapters.isLeadingChapterPreloadMode, + isTrailingChapterPreloadMode: + isTrailingChapterPreloadMode !== undefined + ? isTrailingChapterPreloadMode + : state.visibleChapters.isTrailingChapterPreloadMode, scrollIntoView, resumeMode: isPreviousChapter ? ReaderResumeMode.END : ReaderResumeMode.START, }, diff --git a/src/modules/reader/utils/ReaderPager.utils.tsx b/src/modules/reader/utils/ReaderPager.utils.tsx index 51e6c0e7..699823fd 100644 --- a/src/modules/reader/utils/ReaderPager.utils.tsx +++ b/src/modules/reader/utils/ReaderPager.utils.tsx @@ -267,8 +267,9 @@ const getPageDownloadPriority = ( pageIndex: number, totalPages: number, shouldLoad: boolean, + isPreloadMode: boolean, ): number => { - if (!shouldLoad) { + if (!shouldLoad || isPreloadMode) { return Number.MAX_SAFE_INTEGER; } @@ -288,6 +289,7 @@ export const createReaderPage = ( pagesIndex: number, isPrimaryPage: boolean, isLoaded: boolean, + isPreloadMode: boolean, onLoad: ComponentProps['onLoad'], onError: ComponentProps['onError'], shouldLoad: boolean, @@ -317,7 +319,7 @@ export const createReaderPage = ( src={url} alt={alt} display={display} - priority={getPageDownloadPriority(currentPageIndex, index, totalPages, shouldLoad)} + priority={getPageDownloadPriority(currentPageIndex, index, totalPages, shouldLoad, isPreloadMode)} position={position} onLoad={onLoad} onError={onError}