From fb31a92cd7db5344ba0fc1b1b80bff665bc97eac Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:00:46 +0100 Subject: [PATCH] Reader transition page --- public/locales/en.json | 18 +- .../reader/components/ReaderHotkeys.tsx | 6 +- .../desktop/ReaderNavBarDesktop.tsx | 8 +- .../ReaderNavBarDesktopPageNavigation.tsx | 5 +- .../progress-bar/ReaderProgressBar.tsx | 16 +- .../viewer/ReaderTransitionPage.tsx | 185 ++++++++++++++++++ .../reader/components/viewer/ReaderViewer.tsx | 12 +- .../components/viewer/pager/BasePager.tsx | 31 ++- .../viewer/pager/ReaderDoublePagedPager.tsx | 6 +- .../viewer/pager/ReaderHorizontalPager.tsx | 2 +- .../viewer/pager/ReaderPagedPager.tsx | 4 +- .../viewer/pager/ReaderVerticalPager.tsx | 2 +- .../state/ReaderStatePagesContext.tsx | 3 + .../state/ReaderStatePagesContextProvider.tsx | 8 +- src/modules/reader/services/ReaderControls.ts | 146 ++++++++++++-- src/modules/reader/types/Reader.types.ts | 10 +- .../reader/types/ReaderProgressBar.types.ts | 4 +- .../reader/utils/ReaderPager.utils.tsx | 9 +- .../reader/utils/ReaderProgressBar.utils.tsx | 7 +- 19 files changed, 423 insertions(+), 59 deletions(-) create mode 100644 src/modules/reader/components/viewer/ReaderTransitionPage.tsx diff --git a/public/locales/en.json b/public/locales/en.json index 7c88ccbe..d5b4aeb0 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -235,7 +235,7 @@ "label": { "downloading": "Downloading", "error": "Error", - "finished": "Finished", + "finished": "$t(global.label.finished)", "queued": "Queued" } }, @@ -407,6 +407,7 @@ "discord": "Discord", "display": "Display", "filter": "Filter", + "finished": "Finished", "general": "General", "github": "GitHub", "hidden": "Hidden", @@ -805,6 +806,7 @@ }, "error": { "label": { + "chapter_not_found": "Chapter does not exist", "next_chapter_does_not_exist": "There is no next chapter to open", "no_pages_found": "No pages found", "prev_chapter_does_not_exist": "There is no previous chapter to open" @@ -929,7 +931,19 @@ "reader_settings": "Reader Settings" } }, - "title": "Reader — Manga {{mangaId}} Chapter {{chapterIndex}}" + "title": "Reader — Manga {{mangaId}} Chapter {{chapterIndex}}", + "transition_page": { + "current": "Current:", + "exit": { + "manga_page": "Exit to manga page", + "previous_page": "Exit to previous page" + }, + "finished": "$t(global.label.finished):", + "first_chapter": "There is no previous chapter", + "last_chapter": "There is no next chapter", + "next": "$t(global.label.next):", + "previous": "$t(global.label.previous):" + } }, "search": { "error": { diff --git a/src/modules/reader/components/ReaderHotkeys.tsx b/src/modules/reader/components/ReaderHotkeys.tsx index 5af1c0fb..924ffc53 100644 --- a/src/modules/reader/components/ReaderHotkeys.tsx +++ b/src/modules/reader/components/ReaderHotkeys.tsx @@ -65,10 +65,11 @@ export const ReaderHotkeys = ({ CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION[readingMode.value], readingDirection.value, scrollElementRef.current, + openChapter, ReaderScrollAmount.SMALL, ), { preventDefault: true }, - [readingMode.value, readingDirection.value], + [readingMode.value, readingDirection.value, openChapter], ); useHotkeys( hotkeys[ReaderHotkey.SCROLL_FORWARD], @@ -79,10 +80,11 @@ export const ReaderHotkeys = ({ CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION[readingMode.value], readingDirection.value, scrollElementRef.current, + openChapter, ReaderScrollAmount.SMALL, ), { preventDefault: true }, - [readingMode.value, readingDirection.value], + [readingMode.value, readingDirection.value, openChapter], ); useHotkeys(hotkeys[ReaderHotkey.PREVIOUS_CHAPTER], () => openChapter('previous'), [openChapter]); useHotkeys(hotkeys[ReaderHotkey.NEXT_CHAPTER], () => openChapter('next'), [openChapter]); diff --git a/src/modules/reader/components/overlay/navigation/desktop/ReaderNavBarDesktop.tsx b/src/modules/reader/components/overlay/navigation/desktop/ReaderNavBarDesktop.tsx index a79221a8..4fa4617b 100644 --- a/src/modules/reader/components/overlay/navigation/desktop/ReaderNavBarDesktop.tsx +++ b/src/modules/reader/components/overlay/navigation/desktop/ReaderNavBarDesktop.tsx @@ -57,7 +57,7 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes const { setReaderNavBarWidth } = useNavBarContext(); const { manga } = useReaderStateMangaContext(); const { chapters, currentChapter, nextChapter, previousChapter } = useReaderStateChaptersContext(); - const { pages, currentPageIndex, setPageToScrollToIndex } = userReaderStatePagesContext(); + const { pages, currentPageIndex } = userReaderStatePagesContext(); const getOptionForDirection = useGetOptionForDirection(); @@ -126,11 +126,7 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes - + ) => { +}: Pick) => { const { t } = useTranslation(); const openPage = ReaderControls.useOpenPage(); const { readingDirection } = ReaderService.useSettings(); @@ -52,7 +51,7 @@ export const ReaderNavBarDesktopPageNavigation = ({ labelId="reader-nav-bar-desktop-page-select" label={t('reader.page_info.label.page')} value={currentPage.primary.index} - onChange={(e) => setPageToScrollToIndex(e.target.value as number)} + onChange={(e) => openPage(e.target.value as number)} > {pages.map(({ primary: { index }, name }) => ( diff --git a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBar.tsx b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBar.tsx index 0a53611b..c6a3e8b7 100644 --- a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBar.tsx +++ b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBar.tsx @@ -20,7 +20,6 @@ import { ReaderProgressBarSlotsContainer } from '@/modules/reader/components/ove import { ProgressBarHighlightReadPages } from '@/modules/reader/components/overlay/progress-bar/ProgressBarHighlightReadPages.tsx'; import { ReaderProgressBarCurrentPageSlot } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBarCurrentPageSlot.tsx'; import { - getNextPageIndex, getPage, getPageForMousePos, getProgressBarPositionInfo, @@ -29,13 +28,13 @@ import { import { getOptionForDirection as getOptionForDirectionImpl } from '@/theme.tsx'; import { ReaderProgressBarSlotsActionArea } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlotsActionArea.tsx'; import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; +import { ReaderControls } from '@/modules/reader/services/ReaderControls.ts'; export const ReaderProgressBar = ({ totalPages, pages, pageLoadStates, currentPageIndex, - setPageToScrollToIndex, slotProps, slots, createProgressBarSlot, @@ -65,12 +64,13 @@ export const ReaderProgressBar = ({ }; }) => { const { isDragging, setIsDragging } = useReaderProgressBarContext(); + const openPage = ReaderControls.useOpenPage(); + const direction = ReaderService.useGetThemeDirection(); const progressBarRef = useRef(null); const isHorizontalPosition = getProgressBarPositionInfo(progressBarPosition).isHorizontal; const currentPage = useMemo(() => getPage(currentPageIndex, pages), [currentPageIndex, pages]); - const direction = ReaderService.useGetThemeDirection(); const getOptionForDirection = useCallback( (...args: Parameters>) => getOptionForDirectionImpl(args[0], args[1], direction), @@ -78,10 +78,10 @@ export const ReaderProgressBar = ({ ); useHandleProgressDragging( + openPage, progressBarRef, isDragging, currentPage, - setPageToScrollToIndex, pages, progressBarPosition, getOptionForDirection, @@ -101,7 +101,7 @@ export const ReaderProgressBar = ({ ? (slotProps?.progressBarPageTexts?.current?.sx ?? []) : [slotProps?.progressBarPageTexts?.current?.sx]), ]} - onClick={() => setPageToScrollToIndex(getNextPageIndex('previous', currentPage.pagesIndex, pages))} + onClick={() => openPage('previous', 'ltr')} > {currentPage.name} @@ -115,7 +115,7 @@ export const ReaderProgressBar = ({ return; } - setPageToScrollToIndex( + openPage( getPageForMousePos( event.touches[0], progressBarRef.current.getBoundingClientRect(), @@ -132,7 +132,7 @@ export const ReaderProgressBar = ({ return; } - setPageToScrollToIndex( + openPage( getPageForMousePos( event, progressBarRef.current.getBoundingClientRect(), @@ -195,7 +195,7 @@ export const ReaderProgressBar = ({ ? (slotProps?.progressBarPageTexts?.total?.sx ?? []) : [slotProps?.progressBarPageTexts?.total?.sx]), ]} - onClick={() => setPageToScrollToIndex(getNextPageIndex('next', currentPage.pagesIndex, pages))} + onClick={() => openPage('next', 'ltr')} > {totalPages} diff --git a/src/modules/reader/components/viewer/ReaderTransitionPage.tsx b/src/modules/reader/components/viewer/ReaderTransitionPage.tsx new file mode 100644 index 00000000..14d7714e --- /dev/null +++ b/src/modules/reader/components/viewer/ReaderTransitionPage.tsx @@ -0,0 +1,185 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import Typography from '@mui/material/Typography'; +import Stack from '@mui/material/Stack'; +import { useTranslation } from 'react-i18next'; +import Button from '@mui/material/Button'; +import { Link } from 'react-router-dom'; +import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx'; +import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx'; +import { ChapterScanlatorInfo } from '@/modules/chapter/services/Chapters.ts'; +import { TChapterReader } from '@/modules/chapter/Chapter.types.ts'; +import { + IReaderSettings, + ReaderPageScaleMode, + ReaderTransitionPageMode, + ReadingMode, +} from '@/modules/reader/types/Reader.types.ts'; +import { isTransitionPageVisible } from '@/modules/reader/utils/ReaderPager.utils.tsx'; +import { useBackButton } from '@/modules/core/hooks/useBackButton.ts'; +import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx'; +import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts'; +import { isContinuousReadingMode } from '@/modules/reader/utils/ReaderSettings.utils.tsx'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; + +const ChapterInfo = ({ + title, + chapter, +}: { + title: string; + chapter?: Pick & ChapterScanlatorInfo; +}) => { + if (!chapter) { + return null; + } + + return ( + + {title} + + {chapter.name} + + {chapter.scanlator && ( + + {chapter.scanlator} + + )} + + ); +}; + +export const ReaderTransitionPage = ({ + type, + mode, + readingMode, + pageScaleMode, +}: Pick & { + type: Exclude; + mode: ReaderTransitionPageMode; +}) => { + const { t } = useTranslation(); + + const handleBack = useBackButton(); + const { manga } = useReaderStateMangaContext(); + const { currentChapter, nextChapter, previousChapter } = useReaderStateChaptersContext(); + const { scrollbarXSize, scrollbarYSize } = useReaderScrollbarContext(); + const { readerNavBarWidth } = useNavBarContext(); + + const isPreviousType = type === ReaderTransitionPageMode.PREVIOUS; + const isNextType = type === ReaderTransitionPageMode.NEXT; + + const isFirstChapter = !!currentChapter && !previousChapter; + const isLastChapter = !!currentChapter && !nextChapter; + + const isFitWidthPageScaleMode = [ReaderPageScaleMode.SCREEN, ReaderPageScaleMode.WIDTH].includes(pageScaleMode); + + if (!isTransitionPageVisible(type, mode, readingMode)) { + return null; + } + + return ( + + + {isPreviousType && isFirstChapter && ( + {t('reader.transition_page.first_chapter')} + )} + + {isPreviousType && !isFirstChapter && ( + + )} + {!!currentChapter && ( + + )} + {isNextType && !isLastChapter && ( + + )} + + {isNextType && isLastChapter && ( + {t('reader.transition_page.last_chapter')} + )} + {((isPreviousType && isFirstChapter) || (isNextType && isLastChapter)) && ( + + + + + )} + + + ); +}; diff --git a/src/modules/reader/components/viewer/ReaderViewer.tsx b/src/modules/reader/components/viewer/ReaderViewer.tsx index e8f60f2c..bde7acb0 100644 --- a/src/modules/reader/components/viewer/ReaderViewer.tsx +++ b/src/modules/reader/components/viewer/ReaderViewer.tsx @@ -48,6 +48,7 @@ export const ReaderViewer = forwardRef((_, ref: ForwardedRef { + const pageToScrollTo = getPage(pageToScrollToIndex, pages); + if (isContinuousReadingMode(readingMode.value)) { - const pageToScrollToPagesIndex = getPage(pageToScrollToIndex, pages).pagesIndex; - const imageRef = imageRefs.current[pageToScrollToPagesIndex]; + const imageRef = imageRefs.current[pageToScrollTo.pagesIndex]; imageRef?.scrollIntoView({ block: 'start', inline: 'start', }); - setCurrentPageIndex(pageToScrollToPagesIndex); } + + setCurrentPageIndex(pageToScrollTo.primary.index); }, [pageToScrollToIndex]); // invert x and y scrolling for the continuous horizontal reading mode @@ -187,6 +190,7 @@ export const ReaderViewer = forwardRef((_, ref: ForwardedRef { const page = actualPages[pagesIndex]; diff --git a/src/modules/reader/components/viewer/pager/BasePager.tsx b/src/modules/reader/components/viewer/pager/BasePager.tsx index f5ab8ecb..e2db473d 100644 --- a/src/modules/reader/components/viewer/pager/BasePager.tsx +++ b/src/modules/reader/components/viewer/pager/BasePager.tsx @@ -13,11 +13,13 @@ import { getImageWidthStyling, getPageIndexesToLoad } from '@/modules/reader/uti import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts'; import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts'; import { isContinuousReadingMode, isReaderWidthEditable } from '@/modules/reader/utils/ReaderSettings.utils.tsx'; -import { ReaderPagerProps } from '@/modules/reader/types/Reader.types.ts'; +import { ReaderPagerProps, ReaderTransitionPageMode } from '@/modules/reader/types/Reader.types.ts'; +import { ReaderTransitionPage } from '@/modules/reader/components/viewer/ReaderTransitionPage.tsx'; export const BasePager = ({ currentPageIndex, pages, + transitionPageMode, imageRefs, createPage, slots, @@ -26,6 +28,7 @@ export const BasePager = ({ page: ReaderStatePages['pages'][number], pagesIndex: number, shouldLoad: boolean, + shouldDisplay: boolean, setRef: (element: HTMLElement | null) => void, ) => ReactNode; slots?: { stackProps?: StackProps }; @@ -64,12 +67,30 @@ export const BasePager = ({ }), ]} > + {pages.map((page, pagesIndex) => - createPage(page, pagesIndex, pagesIndexesToRender.includes(pagesIndex), (element) => { - // eslint-disable-next-line no-param-reassign - imageRefs.current[pagesIndex] = element; - }), + createPage( + page, + pagesIndex, + pagesIndexesToRender.includes(pagesIndex), + [ReaderTransitionPageMode.NONE, ReaderTransitionPageMode.BOTH].includes(transitionPageMode), + (element) => { + // eslint-disable-next-line no-param-reassign + imageRefs.current[pagesIndex] = element; + }, + ), )} + ); }; diff --git a/src/modules/reader/components/viewer/pager/ReaderDoublePagedPager.tsx b/src/modules/reader/components/viewer/pager/ReaderDoublePagedPager.tsx index 1ce63d9f..3f65baf0 100644 --- a/src/modules/reader/components/viewer/pager/ReaderDoublePagedPager.tsx +++ b/src/modules/reader/components/viewer/pager/ReaderDoublePagedPager.tsx @@ -49,7 +49,7 @@ export const ReaderDoublePagedPager = ({ onLoad, ...props }: ReaderPagerProps) = return ( { + createPage={(page, pagesIndex, shouldLoad, shouldDisplay) => { const { primary, secondary } = page; const currentSecondaryPageIndex = currentPage.secondary?.index ?? currentPage.primary.index; @@ -64,7 +64,7 @@ export const ReaderDoublePagedPager = ({ onLoad, ...props }: ReaderPagerProps) = page, () => onLoad?.(pagesIndex), shouldLoad, - isPrimaryPage, + shouldDisplay && isPrimaryPage, currentPage.primary.index, totalPages, hasSecondaryPage @@ -77,7 +77,7 @@ export const ReaderDoublePagedPager = ({ onLoad, ...props }: ReaderPagerProps) = { ...page, primary: { ...page.secondary! } }, () => onLoad?.(pagesIndex, false), shouldLoad, - isSecondaryPage, + shouldDisplay && isSecondaryPage, currentSecondaryPageIndex, totalPages, getPagePosition('second', themeDirection, readingDirection.value), diff --git a/src/modules/reader/components/viewer/pager/ReaderHorizontalPager.tsx b/src/modules/reader/components/viewer/pager/ReaderHorizontalPager.tsx index e8ce8725..91ede33d 100644 --- a/src/modules/reader/components/viewer/pager/ReaderHorizontalPager.tsx +++ b/src/modules/reader/components/viewer/pager/ReaderHorizontalPager.tsx @@ -24,7 +24,7 @@ export const ReaderHorizontalPager = ({ onLoad, ...props }: ReaderPagerProps) => return ( + createPage={(page, pagesIndex, shouldLoad, _, setRef) => createReaderPage( page, () => onLoad?.(pagesIndex), diff --git a/src/modules/reader/components/viewer/pager/ReaderPagedPager.tsx b/src/modules/reader/components/viewer/pager/ReaderPagedPager.tsx index f3ebede4..da730ba1 100644 --- a/src/modules/reader/components/viewer/pager/ReaderPagedPager.tsx +++ b/src/modules/reader/components/viewer/pager/ReaderPagedPager.tsx @@ -16,12 +16,12 @@ export const ReaderPagedPager = ({ onLoad, ...props }: ReaderPagerProps) => { return ( + createPage={(page, pagesIndex, shouldLoad, shouldDisplay) => createReaderPage( page, () => onLoad?.(pagesIndex), shouldLoad, - currentPageIndex === page.primary.index, + shouldDisplay && currentPageIndex === page.primary.index, currentPageIndex, totalPages, ) diff --git a/src/modules/reader/components/viewer/pager/ReaderVerticalPager.tsx b/src/modules/reader/components/viewer/pager/ReaderVerticalPager.tsx index ebd6218c..60c0a85c 100644 --- a/src/modules/reader/components/viewer/pager/ReaderVerticalPager.tsx +++ b/src/modules/reader/components/viewer/pager/ReaderVerticalPager.tsx @@ -19,7 +19,7 @@ export const ReaderVerticalPager = ({ onLoad, ...props }: ReaderPagerProps) => { return ( + createPage={(page, pagesIndex, shouldLoad, _, setRef) => createReaderPage( page, () => onLoad?.(pagesIndex), diff --git a/src/modules/reader/contexts/state/ReaderStatePagesContext.tsx b/src/modules/reader/contexts/state/ReaderStatePagesContext.tsx index d2922c0b..f609337a 100644 --- a/src/modules/reader/contexts/state/ReaderStatePagesContext.tsx +++ b/src/modules/reader/contexts/state/ReaderStatePagesContext.tsx @@ -8,6 +8,7 @@ import { createContext, useContext } from 'react'; import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts'; +import { ReaderTransitionPageMode } from '@/modules/reader/types/Reader.types.ts'; export const ReaderStatePagesContext = createContext({ totalPages: 0, @@ -22,6 +23,8 @@ export const ReaderStatePagesContext = createContext({ setPageLoadStates: () => undefined, pages: [], setPages: () => undefined, + transitionPageMode: ReaderTransitionPageMode.NONE, + setTransitionPageMode: () => undefined, }); export const userReaderStatePagesContext = () => useContext(ReaderStatePagesContext); diff --git a/src/modules/reader/contexts/state/ReaderStatePagesContextProvider.tsx b/src/modules/reader/contexts/state/ReaderStatePagesContextProvider.tsx index 41b65d30..5cc1f4ad 100644 --- a/src/modules/reader/contexts/state/ReaderStatePagesContextProvider.tsx +++ b/src/modules/reader/contexts/state/ReaderStatePagesContextProvider.tsx @@ -9,6 +9,7 @@ import { ContextType, ReactNode, useMemo, useState } from 'react'; import { ReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx'; import { createPageData } from '@/modules/reader/utils/ReaderPager.utils.tsx'; +import { ReaderTransitionPageMode } from '@/modules/reader/types/Reader.types.ts'; type TContext = ContextType; @@ -19,6 +20,9 @@ export const ReaderStatePagesContextProvider = ({ children }: { children: ReactN const [pageUrls, setPageUrls] = useState([]); const [pageLoadStates, setPageLoadStates] = useState([]); const [pages, setPages] = useState([createPageData('', 0)]); + const [transitionPageMode, setTransitionPageMode] = useState( + ReaderTransitionPageMode.NONE, + ); const value = useMemo( () => ({ @@ -34,8 +38,10 @@ export const ReaderStatePagesContextProvider = ({ children }: { children: ReactN setPageLoadStates, pages, setPages, + transitionPageMode, + setTransitionPageMode, }), - [totalPages, pages, currentPageIndex, pageToScrollToIndex, pageUrls, pageLoadStates], + [totalPages, pages, currentPageIndex, pageToScrollToIndex, pageUrls, pageLoadStates, transitionPageMode], ); return {children}; diff --git a/src/modules/reader/services/ReaderControls.ts b/src/modules/reader/services/ReaderControls.ts index facbf1c1..1c595cad 100644 --- a/src/modules/reader/services/ReaderControls.ts +++ b/src/modules/reader/services/ReaderControls.ts @@ -13,7 +13,12 @@ import { getNextPageIndex, getPage } from '@/modules/reader/utils/ReaderProgress import { getOptionForDirection } from '@/theme.tsx'; import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx'; -import { PageInViewportType, ReadingDirection, ReadingMode } from '@/modules/reader/types/Reader.types.ts'; +import { + PageInViewportType, + ReadingDirection, + ReadingMode, + ReaderTransitionPageMode, +} from '@/modules/reader/types/Reader.types.ts'; import { ScrollDirection, ScrollOffset } from '@/modules/core/Core.types.ts'; import { ReaderScrollAmount } from '@/modules/reader/constants/ReaderSettings.constants.tsx'; import { isPageInViewport } from '@/modules/reader/utils/ReaderPager.utils.tsx'; @@ -45,12 +50,22 @@ export class ReaderControls { direction: ScrollDirection, readingDirection: ReadingDirection, element: HTMLElement, + openChapter: ReturnType<(typeof ReaderControls)['useOpenChapter']>, scrollAmountPercentage: number = ReaderScrollAmount.LARGE, ): void { if (!element) { return; } + const isAtStartY = element.scrollTop === 0; + const isAtEndY = + Math.floor(element.scrollTop) === element.scrollHeight - element.clientHeight || + Math.ceil(element.scrollTop) === element.scrollHeight - element.clientHeight; + const isAtStartX = element.scrollLeft === 0; + const isAtEndX = + Math.floor(element.scrollLeft) === element.scrollWidth - element.clientWidth || + Math.ceil(element.scrollLeft) === element.scrollWidth - element.clientWidth; + const scrollAmount = scrollAmountPercentage / 100; const scrollDirection = SCROLL_DIRECTION_BY_SCROLL_OFFSET_BY_READING_DIRECTION[readingDirection][offset]; @@ -59,12 +74,32 @@ export class ReaderControls { switch (direction) { case ScrollDirection.X: + if (isAtStartX && offset === ScrollOffset.BACKWARD) { + openChapter('previous'); + return; + } + + if (isAtEndX && offset === ScrollOffset.FORWARD) { + openChapter('next'); + return; + } + element.scroll({ left: getNewScrollPosition(element.scrollLeft, element.clientWidth), behavior: 'smooth', }); break; case ScrollDirection.Y: + if (isAtStartY && offset === ScrollOffset.BACKWARD) { + openChapter('previous'); + return; + } + + if (isAtEndY && offset === ScrollOffset.FORWARD) { + openChapter('next'); + return; + } + element.scroll({ top: getNewScrollPosition(element.scrollTop, element.clientHeight), behavior: 'smooth', @@ -76,7 +111,7 @@ export class ReaderControls { } static useOpenChapter(): (offset: 'previous' | 'next') => void { - const { readingDirection } = ReaderService.useSettings(); + const { readingDirection, readingMode } = ReaderService.useSettings(); const { previousChapter, nextChapter } = useReaderStateChaptersContext(); const direction = READING_DIRECTION_TO_DIRECTION[readingDirection.value]; @@ -96,13 +131,16 @@ export class ReaderControls { throw new Error(`Unexpected "offset" (${offset})`); } }, - [direction, openPreviousChapter, openNextChapter], + [direction, openPreviousChapter, openNextChapter, readingMode.value], ); } - static useOpenPage(): (offset: 'previous' | 'next') => void { - const { currentPageIndex, setPageToScrollToIndex, pages } = userReaderStatePagesContext(); - const { readingDirection } = ReaderService.useSettings(); + static useOpenPage(): (page: number | 'previous' | 'next', forceDirection?: Direction) => void { + const { currentPageIndex, setPageToScrollToIndex, pages, transitionPageMode, setTransitionPageMode } = + userReaderStatePagesContext(); + const { previousChapter, nextChapter } = useReaderStateChaptersContext(); + const { readingDirection, readingMode } = ReaderService.useSettings(); + const openChapter = ReaderControls.useOpenChapter(); const currentPage = useMemo(() => getPage(currentPageIndex, pages), [currentPageIndex, pages]); const previousPageIndex = useMemo( @@ -115,20 +153,85 @@ export class ReaderControls { ); const direction = READING_DIRECTION_TO_DIRECTION[readingDirection.value]; + const isFirstPage = currentPageIndex === 0; + const isLastPage = currentPageIndex === pages[pages.length - 1].primary.index; + const isATransitionPageVisible = transitionPageMode !== ReaderTransitionPageMode.NONE; + const isContinuousReadingModeActive = isContinuousReadingMode(readingMode.value); + return useCallback( - (offset) => { - switch (offset) { + (page, forceDirection = direction) => { + const convertedPage = getOptionForDirection( + page, + page === 'previous' ? 'next' : 'previous', + forceDirection, + ); + + const shouldOpenPreviousChapter = + isFirstPage && isATransitionPageVisible && convertedPage === 'previous' && !!previousChapter; + if (shouldOpenPreviousChapter) { + openChapter('previous'); + return; + } + + const shouldOpenNextChapter = + isLastPage && isATransitionPageVisible && convertedPage === 'next' && !!nextChapter; + if (shouldOpenNextChapter) { + openChapter('next'); + return; + } + + const hideTransitionPage = () => setTransitionPageMode(ReaderTransitionPageMode.NONE); + + if (typeof page === 'number') { + setPageToScrollToIndex(page); + hideTransitionPage(); + return; + } + + const needToHideTransitionPage = isATransitionPageVisible && !isContinuousReadingModeActive; + switch (convertedPage) { case 'previous': - setPageToScrollToIndex(getOptionForDirection(previousPageIndex, nextPageIndex, direction)); + if (isFirstPage) { + setTransitionPageMode(ReaderTransitionPageMode.PREVIOUS); + return; + } + + if (needToHideTransitionPage) { + hideTransitionPage(); + return; + } + + setPageToScrollToIndex(previousPageIndex); break; case 'next': - setPageToScrollToIndex(getOptionForDirection(nextPageIndex, previousPageIndex, direction)); + if (isLastPage) { + setTransitionPageMode(ReaderTransitionPageMode.NEXT); + return; + } + + if (needToHideTransitionPage) { + hideTransitionPage(); + return; + } + + setPageToScrollToIndex(nextPageIndex); break; default: - throw new Error(`Unexpected "offset" (${offset})`); + throw new Error(`Unexpected "offset" (${page})`); } }, - [direction, previousPageIndex, nextPageIndex], + [ + direction, + previousPageIndex, + nextPageIndex, + isATransitionPageVisible, + isContinuousReadingModeActive, + isFirstPage, + isLastPage, + openChapter, + !!previousChapter, + !!nextChapter, + ], ); } @@ -153,6 +256,7 @@ export class ReaderControls { const { setShowPreview } = useReaderTapZoneContext(); const { readingMode, readingDirection, isStaticNav } = ReaderService.useSettings(); const openPage = ReaderControls.useOpenPage(); + const openChapter = ReaderControls.useOpenChapter(); return useCallback( (e) => { @@ -177,14 +281,26 @@ export class ReaderControls { break; case TapZoneRegionType.PREVIOUS: if (isContinuousReadingModeActive) { - this.scroll(ScrollOffset.BACKWARD, scrollDirection, readingDirection.value, scrollElement); + this.scroll( + ScrollOffset.BACKWARD, + scrollDirection, + readingDirection.value, + scrollElement, + openChapter, + ); } else { openPage('previous'); } break; case TapZoneRegionType.NEXT: if (isContinuousReadingModeActive) { - this.scroll(ScrollOffset.FORWARD, scrollDirection, readingDirection.value, scrollElement); + this.scroll( + ScrollOffset.FORWARD, + scrollDirection, + readingDirection.value, + scrollElement, + openChapter, + ); } else { openPage('next'); } @@ -193,7 +309,7 @@ export class ReaderControls { throw new Error(`Unexpected "TapZoneRegionType" (${action})`); } }, - [scrollElement, currentPageIndex, pages, readingMode.value, openPage, readingDirection.value], + [scrollElement, currentPageIndex, pages, readingMode.value, openPage, readingDirection.value, openChapter], ); } } diff --git a/src/modules/reader/types/Reader.types.ts b/src/modules/reader/types/Reader.types.ts index 3dee06d8..afecaad7 100644 --- a/src/modules/reader/types/Reader.types.ts +++ b/src/modules/reader/types/Reader.types.ts @@ -197,7 +197,8 @@ export enum ReaderHotkey { OFFSET_SPREAD_PAGES, } -export interface ReaderPagerProps extends Pick { +export interface ReaderPagerProps + extends Pick { imageRefs: MutableRefObject<(HTMLElement | null)[]>; onLoad?: (pagesIndex: number, isPrimary?: boolean) => void; } @@ -206,3 +207,10 @@ export enum PageInViewportType { X, Y, } + +export enum ReaderTransitionPageMode { + NONE, + PREVIOUS, + NEXT, + BOTH, +} diff --git a/src/modules/reader/types/ReaderProgressBar.types.ts b/src/modules/reader/types/ReaderProgressBar.types.ts index 34e8af15..5b4b516a 100644 --- a/src/modules/reader/types/ReaderProgressBar.types.ts +++ b/src/modules/reader/types/ReaderProgressBar.types.ts @@ -8,7 +8,7 @@ import { BoxProps } from '@mui/material/Box'; import { TooltipProps } from '@mui/material/Tooltip'; -import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; +import { IReaderSettings, ReaderTransitionPageMode } from '@/modules/reader/types/Reader.types.ts'; interface SinglePageData { index: number; @@ -35,6 +35,8 @@ export interface ReaderStatePages { setPageLoadStates: React.Dispatch>; pages: PageData[]; setPages: React.Dispatch>; + transitionPageMode: ReaderTransitionPageMode; + setTransitionPageMode: React.Dispatch>; } export interface ReaderProgressBarProps diff --git a/src/modules/reader/utils/ReaderPager.utils.tsx b/src/modules/reader/utils/ReaderPager.utils.tsx index e87433cc..6c1e362e 100644 --- a/src/modules/reader/utils/ReaderPager.utils.tsx +++ b/src/modules/reader/utils/ReaderPager.utils.tsx @@ -13,10 +13,11 @@ import { PageInViewportType, ReaderPageScaleMode, ReadingDirection, + ReaderTransitionPageMode, ReadingMode, } from '@/modules/reader/types/Reader.types.ts'; import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts'; -import { isReaderWidthEditable } from '@/modules/reader/utils/ReaderSettings.utils.tsx'; +import { isContinuousReadingMode, isReaderWidthEditable } from '@/modules/reader/utils/ReaderSettings.utils.tsx'; import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { ReaderPage } from '@/modules/reader/components/viewer/ReaderPage.tsx'; @@ -367,3 +368,9 @@ export const getPageIndexesToLoad = ( .fill(1) .map((_, index) => startPagesIndex + index * directionInvert); }; + +export const isTransitionPageVisible = ( + mode: ReaderTransitionPageMode, + activeMode: ReaderTransitionPageMode, + readingMode: IReaderSettings['readingMode'], +): boolean => [ReaderTransitionPageMode.BOTH, mode].includes(activeMode) || isContinuousReadingMode(readingMode); diff --git a/src/modules/reader/utils/ReaderProgressBar.utils.tsx b/src/modules/reader/utils/ReaderProgressBar.utils.tsx index ffb28da9..05056877 100644 --- a/src/modules/reader/utils/ReaderProgressBar.utils.tsx +++ b/src/modules/reader/utils/ReaderProgressBar.utils.tsx @@ -10,6 +10,7 @@ import { RefObject, useEffect } from 'react'; import { ReaderProgressBarProps, TReaderProgressCurrentPage } from '@/modules/reader/types/ReaderProgressBar.types.ts'; import { getOptionForDirection as getOptionForDirectionImpl } from '@/theme.tsx'; import { ProgressBarPosition } from '@/modules/reader/types/Reader.types.ts'; +import { ReaderControls } from '@/modules/reader/services/ReaderControls.ts'; export const getPage = (pageIndex: number, pages: ReaderProgressBarProps['pages']): TReaderProgressCurrentPage => { const pagesIndex = pages.findIndex(({ primary, secondary }) => @@ -82,10 +83,10 @@ export const getProgressBarPositionInfo = ( }; export const useHandleProgressDragging = ( + openPage: ReturnType<(typeof ReaderControls)['useOpenPage']>, progressBarRef: RefObject, isDragging: boolean, currentPage: TReaderProgressCurrentPage, - setPageToScrollToIndex: (pageIndex: number) => void, pages: ReaderProgressBarProps['pages'], progressBarPosition: ProgressBarPosition, getOptionForDirection: typeof getOptionForDirectionImpl, @@ -115,7 +116,7 @@ export const useHandleProgressDragging = ( return; } - setPageToScrollToIndex(newPageIndex); + openPage(newPageIndex); }; const handleMouseMove = (e: MouseEvent) => { @@ -135,5 +136,5 @@ export const useHandleProgressDragging = ( document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('touchmove', handleTouchMove); }; - }, [isDragging, currentPage, pages, progressBarPosition]); + }, [openPage, isDragging, currentPage, pages, progressBarPosition]); };