From 9d3aeafe0e1bdc7f34206d3f6f09c57e05654280 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Wed, 5 Feb 2025 22:25:35 +0100 Subject: [PATCH] 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. --- .../components/viewer/ReaderChapterViewer.tsx | 5 +- .../ReaderInfiniteScrollUpdateChapter.tsx | 10 +- .../components/viewer/pager/BasePager.tsx | 212 ++++++++++-------- .../viewer/pager/ReaderDoublePagedPager.tsx | 16 +- .../viewer/pager/ReaderHorizontalPager.tsx | 16 +- .../viewer/pager/ReaderPagedPager.tsx | 71 +++--- .../viewer/pager/ReaderVerticalPager.tsx | 73 +++--- .../useReaderInfiniteScrollUpdateChapter.ts | 12 +- .../reader/utils/ReaderSettings.utils.tsx | 4 +- 9 files changed, 211 insertions(+), 208 deletions(-) diff --git a/src/modules/reader/components/viewer/ReaderChapterViewer.tsx b/src/modules/reader/components/viewer/ReaderChapterViewer.tsx index 346cb59e..826bcbd9 100644 --- a/src/modules/reader/components/viewer/ReaderChapterViewer.tsx +++ b/src/modules/reader/components/viewer/ReaderChapterViewer.tsx @@ -149,6 +149,7 @@ const BaseReaderChapterViewer = ({ const ref = useRef(null); const isCurrentChapterRef = useRef(isCurrentChapter); const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null)); + const pagerRef = useRef(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))) && ( )} ; }) => { 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; diff --git a/src/modules/reader/components/viewer/pager/BasePager.tsx b/src/modules/reader/components/viewer/pager/BasePager.tsx index c91e7aa6..1b7c0e22 100644 --- a/src/modules/reader/components/viewer/pager/BasePager.tsx +++ b/src/modules/reader/components/viewer/pager/BasePager.tsx @@ -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 & - Pick & { - 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 & + Pick & { + 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 ( - - {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, - ), - )} - - ); -}; + ...(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, + ), + )} + + ); + }, +); export const BasePager = memo(BaseBasePager); diff --git a/src/modules/reader/components/viewer/pager/ReaderDoublePagedPager.tsx b/src/modules/reader/components/viewer/pager/ReaderDoublePagedPager.tsx index 4da27be3..6d79c3b7 100644 --- a/src/modules/reader/components/viewer/pager/ReaderDoublePagedPager.tsx +++ b/src/modules/reader/components/viewer/pager/ReaderDoublePagedPager.tsx @@ -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) => { +const BaseReaderDoublePagedPager = forwardRef< + HTMLDivElement, + ReaderPagerProps & Pick +>(({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, ...props }, ref) => { const { currentPageIndex, pages, totalPages, readingDirection, pageScaleMode } = props; const { direction: themeDirection } = useTheme(); @@ -57,6 +54,7 @@ const BaseReaderDoublePagedPager = ({ return ( { const { primary, secondary } = page; @@ -129,6 +127,6 @@ const BaseReaderDoublePagedPager = ({ }} /> ); -}; +}); export const ReaderDoublePagedPager = memo(BaseReaderDoublePagedPager); diff --git a/src/modules/reader/components/viewer/pager/ReaderHorizontalPager.tsx b/src/modules/reader/components/viewer/pager/ReaderHorizontalPager.tsx index 232b7e54..0e32b7e5 100644 --- a/src/modules/reader/components/viewer/pager/ReaderHorizontalPager.tsx +++ b/src/modules/reader/components/viewer/pager/ReaderHorizontalPager.tsx @@ -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) => { +const BaseReaderHorizontalPager = forwardRef< + HTMLDivElement, + ReaderPagerProps & Pick +>(({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, ...props }, ref) => { const { currentPageIndex, totalPages, pageGap, readingDirection } = props; const { direction: themeDirection } = useTheme(); @@ -28,6 +25,7 @@ const BaseReaderHorizontalPager = ({ return ( createReaderPage( @@ -70,6 +68,6 @@ const BaseReaderHorizontalPager = ({ }} /> ); -}; +}); export const ReaderHorizontalPager = memo(BaseReaderHorizontalPager); diff --git a/src/modules/reader/components/viewer/pager/ReaderPagedPager.tsx b/src/modules/reader/components/viewer/pager/ReaderPagedPager.tsx index 537cfae7..23ab4194 100644 --- a/src/modules/reader/components/viewer/pager/ReaderPagedPager.tsx +++ b/src/modules/reader/components/viewer/pager/ReaderPagedPager.tsx @@ -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( + ({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, ...props }, ref) => { + const { currentPageIndex, totalPages } = props; - return ( - - 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 ( + + 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); diff --git a/src/modules/reader/components/viewer/pager/ReaderVerticalPager.tsx b/src/modules/reader/components/viewer/pager/ReaderVerticalPager.tsx index beca7ea6..31c292d5 100644 --- a/src/modules/reader/components/viewer/pager/ReaderVerticalPager.tsx +++ b/src/modules/reader/components/viewer/pager/ReaderVerticalPager.tsx @@ -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( + ({ 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 ( - - 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 ( + + 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); diff --git a/src/modules/reader/hooks/useReaderInfiniteScrollUpdateChapter.ts b/src/modules/reader/hooks/useReaderInfiniteScrollUpdateChapter.ts index 9ce6cc23..7c79ee56 100644 --- a/src/modules/reader/hooks/useReaderInfiniteScrollUpdateChapter.ts +++ b/src/modules/reader/hooks/useReaderInfiniteScrollUpdateChapter.ts @@ -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); diff --git a/src/modules/reader/utils/ReaderSettings.utils.tsx b/src/modules/reader/utils/ReaderSettings.utils.tsx index 324ce44a..3a7f1990 100644 --- a/src/modules/reader/utils/ReaderSettings.utils.tsx +++ b/src/modules/reader/utils/ReaderSettings.utils.tsx @@ -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> => { +): MemoExoticComponent>> => { switch (readingMode) { case ReadingMode.SINGLE_PAGE: return ReaderPagedPager;