From 541eeb0445ed26a77b8d64a4106aa6f51f95b743 Mon Sep 17 00:00:00 2001 From: Constantin Piber <59023762+cpiber@users.noreply.github.com> Date: Mon, 24 Mar 2025 23:45:06 +0100 Subject: [PATCH] Reader preserve current page on window resize (#905) * Reader: Reset bounds on window size change 19f2d993 introduced a `minWidth` to make sure sizes are consistent over chapter changes (or when images load later), but when the window itself changes, this makes the reader incorrectly sized * Reader: Update size on navbar width change This happens mostly when the bar is pinned; same effect as previous commit * Reader: Attempt to set the page on resize It's not perfect, but not changing position at all seems impossible due to how resizes are handled by the browser (since we adjust our content to the window size) * Simplify and abstract reader size reset Co-authored-by: schroda <50052685+schroda@users.noreply.github.com> * fix: Reader page full width was not properly calculated for static navbar Only relevant for Fit Content, this did not properly consider the navbar when the user clamps the content to a custom width * Refactor: Put reader min sizes to reader itself instead of chapter This allows some logic simplification. The `ResizeObserver` can needs to be on an element without restrictions; previously, the observer would not see the chapter elements get smaller, since the `min-width` prevented it from shrinking. By applying to the outer element, we still preserve the centering (since `margin: auto` is also applied here), but allow the chapter itself to be smaller. * Reader: Only scroll in continuous reading modes * Reader: clean up size tracking Co-authored-by: schroda <50052685+schroda@users.noreply.github.com> * Fixup previous commit stupidness * Reader: move `minWidth`/`minHeight` back to chapter wrapper * Apply suggestions from code review Co-authored-by: schroda <50052685+schroda@users.noreply.github.com> * Also revert `fullWidth` calculation * Simplify `chapterViewerSize` Co-authored-by: schroda <50052685+schroda@users.noreply.github.com> * Move size logic entirely to hook --------- Co-authored-by: schroda <50052685+schroda@users.noreply.github.com> --- .../components/viewer/ReaderChapterViewer.tsx | 10 ++-- .../reader/components/viewer/ReaderViewer.tsx | 57 ++++++++++++++----- .../hooks/useReaderPreserveScrollPosition.ts | 24 +++++++- 3 files changed, 70 insertions(+), 21 deletions(-) diff --git a/src/modules/reader/components/viewer/ReaderChapterViewer.tsx b/src/modules/reader/components/viewer/ReaderChapterViewer.tsx index 01a21070..9c8bbdf5 100644 --- a/src/modules/reader/components/viewer/ReaderChapterViewer.tsx +++ b/src/modules/reader/components/viewer/ReaderChapterViewer.tsx @@ -125,7 +125,7 @@ const BaseReaderChapterViewer = ({ imageRefs: MutableRefObject<(HTMLElement | null)[]>; scrollIntoView: boolean; resumeMode: ReaderResumeMode; - onSizeChange: (width: number, height: number) => void; + onSizeChange: (width: number, height: number, chapterId: ChapterIdInfo['id']) => void; minWidth: number; minHeight: number; }) => { @@ -243,9 +243,9 @@ const BaseReaderChapterViewer = ({ useCallback( (entries) => { const { clientWidth, clientHeight } = entries[0].target; - onSizeChange(clientWidth, clientHeight); + onSizeChange(clientWidth, clientHeight, chapterId); }, - [onSizeChange, ref.current], + [onSizeChange, ref.current, chapterId], ), ); @@ -376,14 +376,14 @@ const BaseReaderChapterViewer = ({ sx={{ width: 'fit-content', height: 'fit-content', + margin: 'auto', + flexWrap: 'nowrap', ...applyStyles(readingMode === ReadingMode.CONTINUOUS_HORIZONTAL, { minHeight, }), ...applyStyles(isContinuousVerticalReadingMode(readingMode), { minWidth, }), - margin: 'auto', - flexWrap: 'nowrap', ...applyStyles(shouldHideChapter, { maxWidth: 0, maxHeight: 0, diff --git a/src/modules/reader/components/viewer/ReaderViewer.tsx b/src/modules/reader/components/viewer/ReaderViewer.tsx index 042a5d90..9dae69a0 100644 --- a/src/modules/reader/components/viewer/ReaderViewer.tsx +++ b/src/modules/reader/components/viewer/ReaderViewer.tsx @@ -67,6 +67,7 @@ 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'; +import { ChapterIdInfo } from '@/modules/chapter/services/Chapters'; const READING_MODE_TO_IN_VIEWPORT_TYPE: Record = { [ReadingMode.SINGLE_PAGE]: PageInViewportType.X, @@ -184,8 +185,12 @@ const BaseReaderViewer = forwardRef( const handleClick = ReaderControls.useHandleClick(scrollElementRef.current); const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null)); - const chapterViewerSize = useRef({ minChapterViewWidth: 0, minChapterViewHeight: 0 }); - const { minChapterViewWidth, minChapterViewHeight } = chapterViewerSize.current; + const [{ minChapterViewWidth, minChapterViewHeight, minChapterSizeSourceChapterId }, setChapterViewerSize] = + useState({ + minChapterViewWidth: 0, + minChapterViewHeight: 0, + minChapterSizeSourceChapterId: -1, + }); const [, setTriggerReRender] = useState({}); @@ -209,22 +214,41 @@ const BaseReaderViewer = forwardRef( ); const onChapterViewSizeChange = useCallback( - (width: number, height: number) => { + (width: number, height: number, chapterId: ChapterIdInfo['id']) => { if (!isContinuousReadingModeActive) { return; } - if (isContinuousVerticalReadingModeActive && minChapterViewWidth < width) { - chapterViewerSize.current.minChapterViewWidth = width; - setTriggerReRender({}); + const isSameChapterId = chapterId === minChapterSizeSourceChapterId; + + if (isContinuousVerticalReadingModeActive) { + if (!isSameChapterId && minChapterViewWidth >= width) { + return; + } + + setChapterViewerSize({ + minChapterViewWidth: width, + minChapterViewHeight: 0, + minChapterSizeSourceChapterId: chapterId, + }); + return; } - if (minChapterViewHeight < height) { - chapterViewerSize.current.minChapterViewHeight = height; - setTriggerReRender({}); + if (isSameChapterId || minChapterViewHeight < height) { + setChapterViewerSize({ + minChapterViewWidth: 0, + minChapterViewHeight: height, + minChapterSizeSourceChapterId: chapterId, + }); } }, - [isContinuousReadingModeActive, isContinuousVerticalReadingModeActive], + [ + isContinuousReadingModeActive, + isContinuousVerticalReadingModeActive, + minChapterViewWidth, + minChapterViewHeight, + minChapterSizeSourceChapterId, + ], ); useReaderHandlePageSelection( @@ -266,11 +290,16 @@ const BaseReaderViewer = forwardRef( readingMode, isContinuousReadingModeActive, readingDirection, + readerNavBarWidth, setPageToScrollToIndex, ); useLayoutEffect(() => { - chapterViewerSize.current = { minChapterViewWidth: 0, minChapterViewHeight: 0 }; + setChapterViewerSize({ + minChapterViewWidth: 0, + minChapterViewHeight: 0, + minChapterSizeSourceChapterId: -1, + }); setTriggerReRender({}); }, [readingMode]); @@ -341,6 +370,8 @@ const BaseReaderViewer = forwardRef( visibleChapters, ); + const isChapterSizeSourceChapter = chapter.id === minChapterSizeSourceChapterId; + return ( ); })} diff --git a/src/modules/reader/hooks/useReaderPreserveScrollPosition.ts b/src/modules/reader/hooks/useReaderPreserveScrollPosition.ts index 9a21775a..471a0ca1 100644 --- a/src/modules/reader/hooks/useReaderPreserveScrollPosition.ts +++ b/src/modules/reader/hooks/useReaderPreserveScrollPosition.ts @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { RefObject, useEffect, useLayoutEffect, useRef } from 'react'; +import { RefObject, useCallback, useEffect, useLayoutEffect, useRef } from 'react'; import { ChapterIdInfo } from '@/modules/chapter/services/Chapters.ts'; import { ReaderStateChapters, ReadingDirection, ReadingMode } from '@/modules/reader/types/Reader.types.ts'; import { getOptionForDirection } from '@/modules/theme/services/ThemeCreator.ts'; @@ -19,15 +19,17 @@ export const useReaderPreserveScrollPosition = ( scrollElementRef: RefObject, currentChapterId: ChapterIdInfo['id'] | undefined, chapterIndex: number, - currentPageIndex: number, + pageIndex: number, chaptersToRender: TChapterReader[], visibleChapters: ReaderStateChapters['visibleChapters'], readingMode: ReadingMode, isContinuousReadingModeActive: boolean, readingDirection: ReadingDirection, + readerNavBarWidth: number, setPageToScrollToIndex: ReaderStatePages['setPageToScrollToIndex'], ) => { const scrollPosition = useRef({ left: 0, top: 0, scrollWidth: 0, scrollHeight: 0 }); + const readerNavBarWidthRef = useRef(readerNavBarWidth); useEffect(() => { const element = scrollElementRef.current; @@ -96,7 +98,23 @@ export const useReaderPreserveScrollPosition = ( scrollElement.scrollTo(newLeft, newTop); }, [currentChapterId]); + const onSizeReset = useCallback(() => { + if (!isContinuousReadingModeActive) return; + setPageToScrollToIndex(pageIndex); + }, [isContinuousReadingModeActive, pageIndex]); + + useEffect(() => { + window.addEventListener('resize', onSizeReset); + return () => window.removeEventListener('resize', onSizeReset); + }, [onSizeReset]); + + useEffect(() => { + if (readerNavBarWidthRef.current === readerNavBarWidth) return; + onSizeReset(); + readerNavBarWidthRef.current = readerNavBarWidth; + }, [onSizeReset, readerNavBarWidth]); + useLayoutEffect(() => { - setPageToScrollToIndex(currentPageIndex); + setPageToScrollToIndex(pageIndex); }, [readingMode]); };