diff --git a/src/modules/reader/components/viewer/ReaderViewer.tsx b/src/modules/reader/components/viewer/ReaderViewer.tsx index be6f1f15..1b94ce06 100644 --- a/src/modules/reader/components/viewer/ReaderViewer.tsx +++ b/src/modules/reader/components/viewer/ReaderViewer.tsx @@ -59,12 +59,14 @@ import { useReaderHandlePageSelection } from '@/modules/reader/hooks/useReaderHa import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx'; import { ReaderChapterViewer } from '@/modules/reader/components/viewer/ReaderChapterViewer.tsx'; import { + getPreviousNextChapterVisibility, getReaderChapterViewerCurrentPageIndex, getReaderChapterViewResumeMode, } from '@/modules/reader/utils/Reader.utils.ts'; import { coerceIn, noOp } from '@/lib/HelperFunctions.ts'; import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { NavbarContextType } from '@/modules/navigation-bar/NavigationBar.types.ts'; +import { useReaderPreserveScrollPosition } from '@/modules/reader/hooks/useReaderPreserveScrollPosition.ts'; const READING_MODE_TO_IN_VIEWPORT_TYPE: Record = { [ReadingMode.SINGLE_PAGE]: PageInViewportType.X, @@ -254,6 +256,15 @@ const BaseReaderViewer = forwardRef( scrollElementRef, ); useReaderAutoScroll(isOverlayVisible, automaticScrolling); + useReaderPreserveScrollPosition( + scrollElementRef, + currentChapter?.id, + currentChapterIndex, + chaptersToRender, + visibleChapters, + isContinuousReadingModeActive, + readingDirection, + ); useLayoutEffect(() => { chapterViewerSize.current = { minChapterViewWidth: 0, minChapterViewHeight: 0 }; @@ -321,17 +332,11 @@ const BaseReaderViewer = forwardRef( (isLastLeadingChapter && visibleChapters.isLeadingChapterPreloadMode) || (isLastTrailingChapter && visibleChapters.isTrailingChapterPreloadMode); - const isPreviousChapterLoaded = !!chaptersToRender[chapterIndex + 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; + const previousNextChapterVisibility = getPreviousNextChapterVisibility( + chapterIndex, + chaptersToRender, + visibleChapters, + ); return ( , + currentChapterId: ChapterIdInfo['id'] | undefined, + chapterIndex: number, + chaptersToRender: TChapterReader[], + visibleChapters: ReaderStateChapters['visibleChapters'], + isContinuousReadingModeActive: boolean, + readingDirection: ReadingDirection, +) => { + const scrollPosition = useRef({ left: 0, top: 0, scrollWidth: 0, scrollHeight: 0 }); + + useEffect(() => { + const element = scrollElementRef.current; + + if (!element) { + return () => {}; + } + + const onScroll = () => { + scrollPosition.current = { + ...scrollPosition.current, + left: element.scrollLeft, + top: element.scrollTop, + }; + }; + + element.addEventListener('scroll', onScroll); + + return () => element.removeEventListener('scroll', onScroll); + }, []); + + useLayoutEffect(() => { + const scrollElement = scrollElementRef.current; + const { left, top, scrollWidth, scrollHeight } = scrollPosition.current; + + if (!scrollElement || !isContinuousReadingModeActive) { + return; + } + + scrollPosition.current = { + ...scrollPosition.current, + scrollWidth: scrollElement.scrollWidth, + scrollHeight: scrollElement.scrollHeight, + }; + + const themeDirectionForReadingDirection = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection]; + + const previousNextChapterVisibility = getPreviousNextChapterVisibility( + chapterIndex, + chaptersToRender, + visibleChapters, + ); + + const wasScrolledBackwardHorizontal = Math.abs(left) < window.innerWidth * 1.5; + const wasScrolledBackwardVertical = top < window.innerHeight * 1.5; + + const widthOfPrependedContent = scrollElement.scrollWidth - scrollWidth; + const heightOfPrependedContent = scrollElement.scrollHeight - scrollHeight; + + const newLeft = wasScrolledBackwardHorizontal + ? getOptionForDirection( + widthOfPrependedContent, + -widthOfPrependedContent, + themeDirectionForReadingDirection, + ) + left + : left; + const newTop = wasScrolledBackwardVertical ? heightOfPrependedContent + top : top; + + // only relevant when prepending content to the dom due to the resulting layout shift + const isFirstRenderOfPreviousChapter = + !previousNextChapterVisibility.previous && (wasScrolledBackwardHorizontal || wasScrolledBackwardVertical); + if (!isFirstRenderOfPreviousChapter) { + return; + } + + scrollElement.scrollTo(newLeft, newTop); + }, [currentChapterId]); +}; diff --git a/src/modules/reader/utils/Reader.utils.ts b/src/modules/reader/utils/Reader.utils.ts index f20dcf07..2a282df3 100644 --- a/src/modules/reader/utils/Reader.utils.ts +++ b/src/modules/reader/utils/Reader.utils.ts @@ -321,3 +321,25 @@ export const getReaderChapterViewResumeMode = ( return ReaderResumeMode.START; }; + +export const getPreviousNextChapterVisibility = ( + chapterIndex: number, + chaptersToRender: TChapterReader[], + visibleChapters: ReaderStateChapters['visibleChapters'], +): { previous: boolean; next: boolean } => { + const isPreviousChapterLoaded = !!chaptersToRender[chapterIndex + 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 { + previous: isPreviousChapterVisible, + next: isNextChapterVisible, + }; +};