Preload previous/next chapter in reader

This commit is contained in:
schroda
2025-02-10 01:05:35 +01:00
parent 16c1edaa52
commit 174d850093
14 changed files with 185 additions and 44 deletions

View File

@@ -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 = <T,>(
const updateState = <T,>(
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 = ({
}),
}}
>
<ReaderInfiniteScrollUpdateChapter
readingMode={readingMode}
readingDirection={readingDirection}
chapterId={chapterId}
previousChapterId={previousChapterId}
nextChapterId={nextChapterId}
isCurrentChapter={isCurrentChapter}
isPreviousChapterVisible={isPreviousChapterVisible}
isNextChapterVisible={isNextChapterVisible}
imageWrapper={pagerRef.current}
/>
{((!isContinuousReadingModeActive && isCurrentChapter) ||
(isContinuousReadingModeActive && (isInitialChapter || isLeadingChapter))) && (
{!isPreloadMode && (
<ReaderInfiniteScrollUpdateChapter
readingMode={readingMode}
readingDirection={readingDirection}
chapterId={chapterId}
previousChapterId={previousChapterId}
nextChapterId={nextChapterId}
isCurrentChapter={isCurrentChapter}
isPreviousChapterVisible={isPreviousChapterVisible}
isNextChapterVisible={isNextChapterVisible}
imageWrapper={pagerRef.current}
/>
)}
{showPreviousTransitionPage && (
<ReaderTransitionPage chapterId={chapterId} type={ReaderTransitionPageMode.PREVIOUS} />
)}
<Pager
@@ -436,9 +446,9 @@ const BaseReaderChapterViewer = ({
scrollbarXSize={scrollbarXSize}
scrollbarYSize={scrollbarYSize}
readerNavBarWidth={readerNavBarWidth}
isPreloadMode={isPreloadMode}
/>
{((!isContinuousReadingModeActive && isCurrentChapter) ||
(isContinuousReadingModeActive && (isInitialChapter || isTrailingChapter))) && (
{showNextTransitionPage && (
<ReaderTransitionPage chapterId={chapterId} type={ReaderTransitionPageMode.NEXT} />
)}
</Stack>

View File

@@ -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 (
<ReaderChapterViewer
@@ -321,8 +339,8 @@ const BaseReaderViewer = forwardRef(
chapterId={chapter.id}
previousChapterId={previousChapter?.id}
nextChapterId={nextChapter?.id}
isPreviousChapterVisible={!!chaptersToRender[chapterIndex + 1]}
isNextChapterVisible={!!chaptersToRender[chapterIndex - 1]}
isPreviousChapterVisible={isPreviousChapterVisible}
isNextChapterVisible={isNextChapterVisible}
lastPageRead={chapter.lastPageRead}
currentPageIndex={getReaderChapterViewerCurrentPageIndex(
currentPageIndex,
@@ -340,6 +358,7 @@ const BaseReaderViewer = forwardRef(
isNextChapter={isNextChapter}
isLeadingChapter={isLeadingChapter}
isTrailingChapter={isTrailingChapter}
isPreloadMode={isPreloadMode}
imageRefs={imageRefs}
setPages={setPages}
setPageLoadStates={setPageLoadStates}

View File

@@ -16,7 +16,7 @@ import { isContinuousReadingMode } from '@/modules/reader/utils/ReaderSettings.u
const BaseBasePager = forwardRef<
HTMLDivElement,
Omit<ReaderPagerProps, 'pageLoadStates' | 'retryFailedPagesKeyPrefix'> &
Omit<ReaderPagerProps, 'pageLoadStates' | 'retryFailedPagesKeyPrefix' | 'isPreloadMode'> &
Pick<IReaderSettings, 'readingMode' | 'imagePreLoadAmount'> & {
createPage: (
page: ReaderStatePages['pages'][number],

View File

@@ -44,7 +44,7 @@ const getPagePosition = (
const BaseReaderDoublePagedPager = forwardRef<
HTMLDivElement,
ReaderPagerProps & Pick<IReaderSettings, 'readingDirection' | 'pageScaleMode'>
>(({ 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,

View File

@@ -16,7 +16,7 @@ import { createReaderPage } from '@/modules/reader/utils/ReaderPager.utils.tsx';
const BaseReaderHorizontalPager = forwardRef<
HTMLDivElement,
ReaderPagerProps & Pick<IReaderSettings, 'pageGap' | 'readingDirection'>
>(({ 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,

View File

@@ -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<HTMLDivElement, ReaderPagerProps>(
({ 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<HTMLDivElement, ReaderPagerProps>(
pagesIndex,
true,
pageLoadStates[page.primary.index].loaded,
isPreloadMode,
onLoad,
onError,
shouldLoad,

View File

@@ -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<HTMLDivElement, ReaderPagerProps>(
({ 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<HTMLDivElement, ReaderPagerProps>(
pagesIndex,
true,
pageLoadStates[page.primary.index].loaded,
isPreloadMode,
onLoad,
onError,
shouldLoad,
true,
!isPreloadMode,
currentPageIndex,
totalPages,
...baseProps,

View File

@@ -18,6 +18,8 @@ export const READER_STATE_CHAPTERS_DEFAULTS: Omit<ReaderStateChapters, 'setReade
trailing: 0,
lastLeadingChapterSourceOrder: 99999,
lastTrailingChapterSourceOrder: -1,
isLeadingChapterPreloadMode: true,
isTrailingChapterPreloadMode: true,
scrollIntoView: false,
resumeMode: undefined,
},

View File

@@ -81,6 +81,8 @@ export const useReaderSetChaptersState = (
lastTrailingChapterSourceOrder:
newInitialChapter?.sourceOrder ??
READER_STATE_CHAPTERS_DEFAULTS.visibleChapters.lastTrailingChapterSourceOrder,
isLeadingChapterPreloadMode: false,
isTrailingChapterPreloadMode: false,
}
: prevState.visibleChapters,
};

View File

@@ -236,6 +236,8 @@ export class ReaderControls {
prevState,
chapterToOpen.sourceOrder,
scrollIntoView,
isPreviousChapter ? false : undefined,
!isPreviousChapter ? false : undefined,
),
);
@@ -448,8 +450,15 @@ export class ReaderControls {
endReached?: boolean,
) => 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,
],
);
}

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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<ReaderStateChapters, 'setReaderStateChapters'>,
chapterToOpenSourceOrder: TChapterReader['sourceOrder'],
scrollIntoView: boolean,
isLeadingChapterPreloadMode?: boolean,
isTrailingChapterPreloadMode?: boolean,
): Omit<ReaderStateChapters, 'setReaderStateChapters'> => {
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,
},

View File

@@ -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<typeof ReaderPage>['onLoad'],
onError: ComponentProps<typeof ReaderPage>['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}