Use "BasePager" for infinite scroll chapter change detection

There can be cases where when scrolling backward/forward the first/last page never gets visible in the viewport due to a large width/height of another page.
In that case the intersection observer would never trigger and thus, the chapter wouldn't get changed.

By using the Pager, which always takes up the full width/height, this problem will not be possible, since the start/end of the Pager always enters or leaves the viewport.
This commit is contained in:
schroda
2025-02-05 22:25:35 +01:00
parent b12e28346c
commit 9d3aeafe0e
9 changed files with 211 additions and 208 deletions

View File

@@ -149,6 +149,7 @@ const BaseReaderChapterViewer = ({
const ref = useRef<HTMLDivElement>(null);
const isCurrentChapterRef = useRef(isCurrentChapter);
const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null));
const pagerRef = useRef<HTMLDivElement>(null);
const actualPages = useMemo(() => {
const arePagesLoaded = !!totalPages;
@@ -404,14 +405,14 @@ const BaseReaderChapterViewer = ({
isCurrentChapter={isCurrentChapter}
isPreviousChapterVisible={isPreviousChapterVisible}
isNextChapterVisible={isNextChapterVisible}
firstImage={imageRefs.current[0]}
lastImage={imageRefs.current[imageRefs.current.length - 1]}
imageWrapper={pagerRef.current}
/>
{((!isContinuousReadingModeActive && isCurrentChapter) ||
(isContinuousReadingModeActive && (isInitialChapter || isLeadingChapter))) && (
<ReaderTransitionPage chapterId={chapterId} type={ReaderTransitionPageMode.PREVIOUS} />
)}
<Pager
ref={pagerRef}
totalPages={totalPages}
currentPageIndex={currentPageIndex}
pages={actualPages}

View File

@@ -22,8 +22,7 @@ const BaseReaderInfiniteScrollUpdateChapter = ({
isPreviousChapterVisible,
isCurrentChapter,
isNextChapterVisible,
firstImage,
lastImage,
imageWrapper,
openChapter,
}: {
readingMode: ReadingMode;
@@ -34,8 +33,7 @@ const BaseReaderInfiniteScrollUpdateChapter = ({
isPreviousChapterVisible: boolean;
isCurrentChapter: boolean;
isNextChapterVisible: boolean;
firstImage: HTMLElement | null;
lastImage: HTMLElement | null;
imageWrapper: HTMLElement | null;
openChapter: ReturnType<typeof ReaderControls.useOpenChapter>;
}) => {
useReaderInfiniteScrollUpdateChapter(
@@ -47,7 +45,7 @@ const BaseReaderInfiniteScrollUpdateChapter = ({
readingMode,
readingDirection,
openChapter,
firstImage,
imageWrapper,
);
useReaderInfiniteScrollUpdateChapter(
'last',
@@ -58,7 +56,7 @@ const BaseReaderInfiniteScrollUpdateChapter = ({
readingMode,
readingDirection,
openChapter,
lastImage,
imageWrapper,
);
return null;

View File

@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { memo, ReactNode, useCallback, useEffect, useMemo, useRef } from 'react';
import { forwardRef, memo, ReactNode, useCallback, useEffect, useMemo, useRef } from 'react';
import Box, { BoxProps } from '@mui/material/Box';
import { getPageIndexesToLoad, isATransitionPageVisible } from '@/modules/reader/utils/ReaderPager.utils.tsx';
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
@@ -14,112 +14,130 @@ import { IReaderSettings, ReaderPagerProps, ReaderTransitionPageMode } from '@/m
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
import { isContinuousReadingMode } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
const BaseBasePager = ({
currentPageIndex,
pages,
transitionPageMode,
imageRefs,
createPage,
slots,
readingMode,
imagePreLoadAmount,
isCurrentChapter,
isPreviousChapter,
isNextChapter,
customFilter,
pageScaleMode,
shouldStretchPage,
readerWidth,
scrollbarXSize,
scrollbarYSize,
readerNavBarWidth,
}: Omit<ReaderPagerProps, 'pageLoadStates' | 'retryFailedPagesKeyPrefix'> &
Pick<IReaderSettings, 'readingMode' | 'imagePreLoadAmount'> & {
createPage: (
page: ReaderStatePages['pages'][number],
pagesIndex: number,
shouldLoad: boolean,
shouldDisplay: boolean,
setRef: (pagesIndex: number, element: HTMLElement | null) => void,
readingMode: ReaderPagerProps['readingMode'],
customFilter: ReaderPagerProps['customFilter'],
pageScaleMode: ReaderPagerProps['pageScaleMode'],
shouldStretchPage: ReaderPagerProps['shouldStretchPage'],
readerWidth: ReaderPagerProps['readerWidth'],
scrollbarXSize: ReaderPagerProps['scrollbarXSize'],
scrollbarYSize: ReaderPagerProps['scrollbarYSize'],
readerNavBarWidth: ReaderPagerProps['readerNavBarWidth'],
) => ReactNode;
slots?: { boxProps?: BoxProps };
}) => {
const previousCurrentPageIndex = useRef(-1);
const pagesIndexesToRender = useMemo(
() =>
getPageIndexesToLoad(
const BaseBasePager = forwardRef<
HTMLDivElement,
Omit<ReaderPagerProps, 'pageLoadStates' | 'retryFailedPagesKeyPrefix'> &
Pick<IReaderSettings, 'readingMode' | 'imagePreLoadAmount'> & {
createPage: (
page: ReaderStatePages['pages'][number],
pagesIndex: number,
shouldLoad: boolean,
shouldDisplay: boolean,
setRef: (pagesIndex: number, element: HTMLElement | null) => void,
readingMode: ReaderPagerProps['readingMode'],
customFilter: ReaderPagerProps['customFilter'],
pageScaleMode: ReaderPagerProps['pageScaleMode'],
shouldStretchPage: ReaderPagerProps['shouldStretchPage'],
readerWidth: ReaderPagerProps['readerWidth'],
scrollbarXSize: ReaderPagerProps['scrollbarXSize'],
scrollbarYSize: ReaderPagerProps['scrollbarYSize'],
readerNavBarWidth: ReaderPagerProps['readerNavBarWidth'],
) => ReactNode;
slots?: { boxProps?: BoxProps };
}
>(
(
{
currentPageIndex,
pages,
transitionPageMode,
imageRefs,
createPage,
slots,
readingMode,
imagePreLoadAmount,
isCurrentChapter,
isPreviousChapter,
isNextChapter,
customFilter,
pageScaleMode,
shouldStretchPage,
readerWidth,
scrollbarXSize,
scrollbarYSize,
readerNavBarWidth,
},
ref,
) => {
const previousCurrentPageIndex = useRef(-1);
const pagesIndexesToRender = useMemo(
() =>
getPageIndexesToLoad(
currentPageIndex,
pages,
previousCurrentPageIndex.current,
imagePreLoadAmount,
readingMode,
isCurrentChapter,
isPreviousChapter,
isNextChapter,
),
[
currentPageIndex,
pages,
previousCurrentPageIndex.current,
imagePreLoadAmount,
readingMode,
isCurrentChapter,
isPreviousChapter,
isNextChapter,
),
[currentPageIndex, pages, imagePreLoadAmount, readingMode, isCurrentChapter, isPreviousChapter, isNextChapter],
);
useEffect(() => {
previousCurrentPageIndex.current = currentPageIndex;
}, [pagesIndexesToRender]);
],
);
useEffect(() => {
previousCurrentPageIndex.current = currentPageIndex;
}, [pagesIndexesToRender]);
const setRef = useCallback(
(pagesIndex: number, element: HTMLElement | null) => {
// eslint-disable-next-line no-param-reassign
imageRefs.current[pagesIndex] = element;
},
[imageRefs],
);
const setRef = useCallback(
(pagesIndex: number, element: HTMLElement | null) => {
// eslint-disable-next-line no-param-reassign
imageRefs.current[pagesIndex] = element;
},
[imageRefs],
);
return (
<Box
{...slots?.boxProps}
sx={[
{
width: 'fit-content',
height: 'fit-content',
},
...(Array.isArray(slots?.boxProps?.sx) ? (slots?.boxProps?.sx ?? []) : [slots?.boxProps?.sx]),
// hide pager, without actually unmounting it to prevent re-renders, while a chapter transition page is taking up the full screen
applyStyles(
!isContinuousReadingMode(readingMode) && isATransitionPageVisible(transitionPageMode, readingMode),
return (
<Box
ref={ref}
{...slots?.boxProps}
sx={[
{
visibility: 'hidden',
width: 0,
height: 0,
m: 0,
p: 0,
width: 'fit-content',
height: 'fit-content',
},
),
]}
>
{pages.map((page, pagesIndex) =>
createPage(
page,
pagesIndex,
pagesIndexesToRender.includes(pagesIndex),
[ReaderTransitionPageMode.NONE, ReaderTransitionPageMode.BOTH].includes(transitionPageMode),
setRef,
readingMode,
customFilter,
pageScaleMode,
shouldStretchPage,
readerWidth,
scrollbarXSize,
scrollbarYSize,
readerNavBarWidth,
),
)}
</Box>
);
};
...(Array.isArray(slots?.boxProps?.sx) ? (slots?.boxProps?.sx ?? []) : [slots?.boxProps?.sx]),
// hide pager, without actually unmounting it to prevent re-renders, while a chapter transition page is taking up the full screen
applyStyles(
!isContinuousReadingMode(readingMode) &&
isATransitionPageVisible(transitionPageMode, readingMode),
{
visibility: 'hidden',
width: 0,
height: 0,
m: 0,
p: 0,
},
),
]}
>
{pages.map((page, pagesIndex) =>
createPage(
page,
pagesIndex,
pagesIndexesToRender.includes(pagesIndex),
[ReaderTransitionPageMode.NONE, ReaderTransitionPageMode.BOTH].includes(transitionPageMode),
setRef,
readingMode,
customFilter,
pageScaleMode,
shouldStretchPage,
readerWidth,
scrollbarXSize,
scrollbarYSize,
readerNavBarWidth,
),
)}
</Box>
);
},
);
export const BasePager = memo(BaseBasePager);

View File

@@ -7,7 +7,7 @@
*/
import { Direction, useTheme } from '@mui/material/styles';
import { Fragment, memo, useMemo } from 'react';
import { forwardRef, Fragment, memo, useMemo } from 'react';
import { BasePager } from '@/modules/reader/components/viewer/pager/BasePager.tsx';
import {
IReaderSettings,
@@ -41,13 +41,10 @@ const getPagePosition = (
return isLtrReadingDirection ? 'right' : 'left';
};
const BaseReaderDoublePagedPager = ({
onLoad,
onError,
pageLoadStates,
retryFailedPagesKeyPrefix,
...props
}: ReaderPagerProps & Pick<IReaderSettings, 'readingDirection' | 'pageScaleMode'>) => {
const BaseReaderDoublePagedPager = forwardRef<
HTMLDivElement,
ReaderPagerProps & Pick<IReaderSettings, 'readingDirection' | 'pageScaleMode'>
>(({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, ...props }, ref) => {
const { currentPageIndex, pages, totalPages, readingDirection, pageScaleMode } = props;
const { direction: themeDirection } = useTheme();
@@ -57,6 +54,7 @@ const BaseReaderDoublePagedPager = ({
return (
<BasePager
ref={ref}
{...props}
createPage={(page, pagesIndex, shouldLoad, shouldDisplay, _setRef, ...baseProps) => {
const { primary, secondary } = page;
@@ -129,6 +127,6 @@ const BaseReaderDoublePagedPager = ({
}}
/>
);
};
});
export const ReaderDoublePagedPager = memo(BaseReaderDoublePagedPager);

View File

@@ -7,19 +7,16 @@
*/
import { useTheme } from '@mui/material/styles';
import { memo } from 'react';
import { forwardRef, memo } from 'react';
import { BasePager } from '@/modules/reader/components/viewer/pager/BasePager.tsx';
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
import { IReaderSettings, ReaderPagerProps, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
import { createReaderPage } from '@/modules/reader/utils/ReaderPager.utils.tsx';
const BaseReaderHorizontalPager = ({
onLoad,
onError,
pageLoadStates,
retryFailedPagesKeyPrefix,
...props
}: ReaderPagerProps & Pick<IReaderSettings, 'pageGap' | 'readingDirection'>) => {
const BaseReaderHorizontalPager = forwardRef<
HTMLDivElement,
ReaderPagerProps & Pick<IReaderSettings, 'pageGap' | 'readingDirection'>
>(({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, ...props }, ref) => {
const { currentPageIndex, totalPages, pageGap, readingDirection } = props;
const { direction: themeDirection } = useTheme();
@@ -28,6 +25,7 @@ const BaseReaderHorizontalPager = ({
return (
<BasePager
ref={ref}
{...props}
createPage={(page, pagesIndex, shouldLoad, _, setRef, ...baseProps) =>
createReaderPage(
@@ -70,6 +68,6 @@ const BaseReaderHorizontalPager = ({
}}
/>
);
};
});
export const ReaderHorizontalPager = memo(BaseReaderHorizontalPager);

View File

@@ -6,48 +6,45 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { memo } from 'react';
import { forwardRef, memo } from 'react';
import { BasePager } from '@/modules/reader/components/viewer/pager/BasePager.tsx';
import { ReaderPagerProps } from '@/modules/reader/types/Reader.types.ts';
import { createReaderPage } from '@/modules/reader/utils/ReaderPager.utils.tsx';
const BaseReaderPagedPager = ({
onLoad,
onError,
pageLoadStates,
retryFailedPagesKeyPrefix,
...props
}: ReaderPagerProps) => {
const { currentPageIndex, totalPages } = props;
const BaseReaderPagedPager = forwardRef<HTMLDivElement, ReaderPagerProps>(
({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, ...props }, ref) => {
const { currentPageIndex, totalPages } = props;
return (
<BasePager
{...props}
createPage={(page, pagesIndex, shouldLoad, shouldDisplay, _setRef, ...baseProps) =>
createReaderPage(
page,
pagesIndex,
true,
pageLoadStates[page.primary.index].loaded,
onLoad,
onError,
shouldLoad,
shouldDisplay && shouldLoad && currentPageIndex === page.primary.index,
currentPageIndex,
totalPages,
...baseProps,
pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined,
)
}
slots={{
boxProps: {
sx: {
margin: 'auto',
return (
<BasePager
ref={ref}
{...props}
createPage={(page, pagesIndex, shouldLoad, shouldDisplay, _setRef, ...baseProps) =>
createReaderPage(
page,
pagesIndex,
true,
pageLoadStates[page.primary.index].loaded,
onLoad,
onError,
shouldLoad,
shouldDisplay && shouldLoad && currentPageIndex === page.primary.index,
currentPageIndex,
totalPages,
...baseProps,
pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined,
)
}
slots={{
boxProps: {
sx: {
margin: 'auto',
},
},
},
}}
/>
);
};
}}
/>
);
},
);
export const ReaderPagedPager = memo(BaseReaderPagedPager);

View File

@@ -6,49 +6,46 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { memo } from 'react';
import { forwardRef, memo } from 'react';
import { BasePager } from '@/modules/reader/components/viewer/pager/BasePager.tsx';
import { ReaderPagerProps, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
import { createReaderPage } from '@/modules/reader/utils/ReaderPager.utils.tsx';
const BaseReaderVerticalPager = ({
onLoad,
onError,
pageLoadStates,
retryFailedPagesKeyPrefix,
...props
}: ReaderPagerProps) => {
const { currentPageIndex, totalPages, readingMode, pageGap } = props;
const BaseReaderVerticalPager = forwardRef<HTMLDivElement, ReaderPagerProps>(
({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, ...props }, ref) => {
const { currentPageIndex, totalPages, readingMode, pageGap } = props;
const isWebtoonMode = readingMode === ReadingMode.WEBTOON;
const actualPageGap = isWebtoonMode ? 0 : pageGap;
const isWebtoonMode = readingMode === ReadingMode.WEBTOON;
const actualPageGap = isWebtoonMode ? 0 : pageGap;
return (
<BasePager
{...props}
createPage={(page, pagesIndex, shouldLoad, _, setRef, ...baseProps) =>
createReaderPage(
page,
pagesIndex,
true,
pageLoadStates[page.primary.index].loaded,
onLoad,
onError,
shouldLoad,
true,
currentPageIndex,
totalPages,
...baseProps,
pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined,
undefined,
undefined,
page.primary.index !== 0 ? actualPageGap : 0,
setRef,
)
}
slots={{ boxProps: { sx: { margin: 'auto' } } }}
/>
);
};
return (
<BasePager
ref={ref}
{...props}
createPage={(page, pagesIndex, shouldLoad, _, setRef, ...baseProps) =>
createReaderPage(
page,
pagesIndex,
true,
pageLoadStates[page.primary.index].loaded,
onLoad,
onError,
shouldLoad,
true,
currentPageIndex,
totalPages,
...baseProps,
pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined,
undefined,
undefined,
page.primary.index !== 0 ? actualPageGap : 0,
setRef,
)
}
slots={{ boxProps: { sx: { margin: 'auto' } } }}
/>
);
},
);
export const ReaderVerticalPager = memo(BaseReaderVerticalPager);

View File

@@ -195,7 +195,6 @@ export const useReaderInfiniteScrollUpdateChapter = (
// gets immediately observed once on initial render
let isInitialObserve = true;
let wasNextChapterOpened = false;
const intersectionObserver = new IntersectionObserver(
(entries) => {
if (isInitialObserve) {
@@ -203,10 +202,6 @@ export const useReaderInfiniteScrollUpdateChapter = (
return;
}
if (wasNextChapterOpened) {
return;
}
const entry = entries[entries.length - 1];
const elementIntersectionInfo = getElementIntersectionInfo(
@@ -233,16 +228,17 @@ export const useReaderInfiniteScrollUpdateChapter = (
const openChapterToOpen = initialOpenPreviousChapter || openNextChapter;
if (openChapterToOpen) {
openChapter(chapterToOpenId, !isChapterToOpenVisible, false);
wasNextChapterOpened = true;
return;
}
if (openPreviousChapter) {
openChapter(chapterId, false, false);
wasNextChapterOpened = true;
}
},
{ threshold: [OPEN_CHAPTER_INTERSECTION_RATIO], rootMargin: '-10px 0px 0px 0px' },
{
threshold: [OPEN_CHAPTER_INTERSECTION_RATIO],
rootMargin: pageType === 'first' ? '0px 0px -10px 0px' : '-10px 0px 0px 0px',
},
);
intersectionObserver.observe(image);

View File

@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { ComponentType, MemoExoticComponent } from 'react';
import { ForwardRefExoticComponent, MemoExoticComponent, RefAttributes } from 'react';
import {
IReaderSettings,
IReaderSettingsWithDefaultFlag,
@@ -59,7 +59,7 @@ export const isAutoWebtoonMode = (
export const getPagerForReadingMode = (
readingMode: ReadingMode,
): MemoExoticComponent<ComponentType<ReaderPagerProps>> => {
): MemoExoticComponent<ForwardRefExoticComponent<ReaderPagerProps & RefAttributes<HTMLDivElement>>> => {
switch (readingMode) {
case ReadingMode.SINGLE_PAGE:
return ReaderPagedPager;