From 7434ed0152eb9ebad27682fc19f7b79a71bf9bce Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sat, 28 Sep 2024 03:24:16 +0200 Subject: [PATCH] Reader progress bars --- src/lib/graphql/fragments/ChapterFragments.ts | 1 + src/lib/graphql/generated/graphql.ts | 4 +- src/lib/requests/RequestManager.ts | 2 +- src/modules/chapter/Chapter.types.ts | 3 + src/modules/chapter/services/Chapters.ts | 5 + src/modules/core/contexts/AppContext.tsx | 18 +- src/modules/core/utils/ApplyStyles.ts | 15 + src/modules/core/utils/MediaQuery.tsx | 41 ++- src/modules/metadata/Metadata.constants.ts | 3 + .../ProgressBarHighlightReadPages.tsx | 39 +++ .../progress-bar/ReaderProgressBar.tsx | 205 ++++++++++++++ .../ReaderProgressBarContainer.tsx | 36 +++ .../ReaderProgressBarCurrentPageSlot.tsx | 71 +++++ .../ReaderProgressBarDirectionWrapper.tsx | 42 +++ .../ReaderProgressBarPageNumber.tsx | 22 ++ .../progress-bar/ReaderProgressBarRoot.tsx | 17 ++ .../progress-bar/ReaderProgressBarSlot.tsx | 31 +++ .../ReaderProgressBarSlotsActionArea.tsx | 16 ++ .../ReaderProgressBarSlotsContainer.tsx | 16 ++ .../variants/MobileReaderProgressBar.tsx | 179 ++++++++++++ .../variants/StandardReaderProgressBar.tsx | 257 ++++++++++++++++++ .../reader/constants/Reader.constants.ts | 10 +- .../constants/ReaderProgressBar.constants.ts | 16 ++ .../reader/contexts/ReaderContextProvider.tsx | 14 +- .../reader/contexts/ReaderOverlayContext.tsx | 21 ++ .../contexts/ReaderOverlayContextProvider.tsx | 18 ++ .../contexts/ReaderProgressBarContext.tsx | 25 ++ .../ReaderProgressBarContextProvider.tsx | 22 ++ .../contexts/ReaderScrollbarContext.tsx | 25 ++ .../ReaderScrollbarContextProvider.tsx | 22 ++ .../state/ReaderStateChaptersContext.tsx | 17 ++ .../ReaderStateChaptersContextProvider.tsx | 26 ++ .../state/ReaderStateContextProvider.tsx | 8 +- .../state/ReaderStatePagesContext.tsx | 25 ++ .../state/ReaderStatePagesContextProvider.tsx | 39 +++ src/modules/reader/services/ReaderService.ts | 39 ++- .../reader/services/ReaderSettingsMetadata.ts | 18 ++ src/modules/reader/types/Reader.types.ts | 23 ++ .../reader/types/ReaderProgressBar.types.ts | 61 +++++ .../reader/utils/ReaderProgressBar.utils.tsx | 139 ++++++++++ src/modules/theme/ThemeDirectionCache.ts | 23 ++ src/theme.tsx | 7 +- 42 files changed, 1587 insertions(+), 34 deletions(-) create mode 100644 src/modules/core/utils/ApplyStyles.ts create mode 100644 src/modules/reader/components/overlay/progress-bar/ProgressBarHighlightReadPages.tsx create mode 100644 src/modules/reader/components/overlay/progress-bar/ReaderProgressBar.tsx create mode 100644 src/modules/reader/components/overlay/progress-bar/ReaderProgressBarContainer.tsx create mode 100644 src/modules/reader/components/overlay/progress-bar/ReaderProgressBarCurrentPageSlot.tsx create mode 100644 src/modules/reader/components/overlay/progress-bar/ReaderProgressBarDirectionWrapper.tsx create mode 100644 src/modules/reader/components/overlay/progress-bar/ReaderProgressBarPageNumber.tsx create mode 100644 src/modules/reader/components/overlay/progress-bar/ReaderProgressBarRoot.tsx create mode 100644 src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlot.tsx create mode 100644 src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlotsActionArea.tsx create mode 100644 src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlotsContainer.tsx create mode 100644 src/modules/reader/components/overlay/progress-bar/variants/MobileReaderProgressBar.tsx create mode 100644 src/modules/reader/components/overlay/progress-bar/variants/StandardReaderProgressBar.tsx create mode 100644 src/modules/reader/constants/ReaderProgressBar.constants.ts create mode 100644 src/modules/reader/contexts/ReaderOverlayContext.tsx create mode 100644 src/modules/reader/contexts/ReaderOverlayContextProvider.tsx create mode 100644 src/modules/reader/contexts/ReaderProgressBarContext.tsx create mode 100644 src/modules/reader/contexts/ReaderProgressBarContextProvider.tsx create mode 100644 src/modules/reader/contexts/ReaderScrollbarContext.tsx create mode 100644 src/modules/reader/contexts/ReaderScrollbarContextProvider.tsx create mode 100644 src/modules/reader/contexts/state/ReaderStateChaptersContext.tsx create mode 100644 src/modules/reader/contexts/state/ReaderStateChaptersContextProvider.tsx create mode 100644 src/modules/reader/contexts/state/ReaderStatePagesContext.tsx create mode 100644 src/modules/reader/contexts/state/ReaderStatePagesContextProvider.tsx create mode 100644 src/modules/reader/types/ReaderProgressBar.types.ts create mode 100644 src/modules/reader/utils/ReaderProgressBar.utils.tsx create mode 100644 src/modules/theme/ThemeDirectionCache.ts diff --git a/src/lib/graphql/fragments/ChapterFragments.ts b/src/lib/graphql/fragments/ChapterFragments.ts index 5ce20cd6..b6886b3c 100644 --- a/src/lib/graphql/fragments/ChapterFragments.ts +++ b/src/lib/graphql/fragments/ChapterFragments.ts @@ -48,6 +48,7 @@ export const CHAPTER_READER_FIELDS = gql` ...CHAPTER_BASE_FIELDS ...CHAPTER_STATE_FIELDS + uploadDate lastPageRead pageCount } diff --git a/src/lib/graphql/generated/graphql.ts b/src/lib/graphql/generated/graphql.ts index e7bea56c..5b23b72a 100644 --- a/src/lib/graphql/generated/graphql.ts +++ b/src/lib/graphql/generated/graphql.ts @@ -2767,7 +2767,7 @@ export type ChapterBaseFieldsFragment = { __typename?: 'ChapterType', id: number export type ChapterStateFieldsFragment = { __typename?: 'ChapterType', id: number, isRead: boolean, isDownloaded: boolean, isBookmarked: boolean }; -export type ChapterReaderFieldsFragment = { __typename?: 'ChapterType', lastPageRead: number, pageCount: number, id: number, name: string, mangaId: number, scanlator?: string | null, realUrl?: string | null, sourceOrder: number, chapterNumber: number, isRead: boolean, isDownloaded: boolean, isBookmarked: boolean }; +export type ChapterReaderFieldsFragment = { __typename?: 'ChapterType', uploadDate: string, lastPageRead: number, pageCount: number, id: number, name: string, mangaId: number, scanlator?: string | null, realUrl?: string | null, sourceOrder: number, chapterNumber: number, isRead: boolean, isDownloaded: boolean, isBookmarked: boolean }; export type ChapterListFieldsFragment = { __typename?: 'ChapterType', fetchedAt: string, uploadDate: string, id: number, name: string, mangaId: number, scanlator?: string | null, realUrl?: string | null, sourceOrder: number, chapterNumber: number, isRead: boolean, isDownloaded: boolean, isBookmarked: boolean }; @@ -3349,7 +3349,7 @@ export type GetChaptersReaderQueryVariables = Exact<{ }>; -export type GetChaptersReaderQuery = { __typename?: 'Query', chapters: { __typename?: 'ChapterNodeList', totalCount: number, nodes: Array<{ __typename?: 'ChapterType', lastPageRead: number, pageCount: number, id: number, name: string, mangaId: number, scanlator?: string | null, realUrl?: string | null, sourceOrder: number, chapterNumber: number, isRead: boolean, isDownloaded: boolean, isBookmarked: boolean }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null } } }; +export type GetChaptersReaderQuery = { __typename?: 'Query', chapters: { __typename?: 'ChapterNodeList', totalCount: number, nodes: Array<{ __typename?: 'ChapterType', uploadDate: string, lastPageRead: number, pageCount: number, id: number, name: string, mangaId: number, scanlator?: string | null, realUrl?: string | null, sourceOrder: number, chapterNumber: number, isRead: boolean, isDownloaded: boolean, isBookmarked: boolean }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null } } }; export type GetChaptersMangaQueryVariables = Exact<{ after?: InputMaybe; diff --git a/src/lib/requests/RequestManager.ts b/src/lib/requests/RequestManager.ts index fa9695a9..791c5350 100644 --- a/src/lib/requests/RequestManager.ts +++ b/src/lib/requests/RequestManager.ts @@ -2184,7 +2184,7 @@ export class RequestManager { return this.doRequest(GQLMethod.QUERY, document, variables, options); } - public useGetMangaChapters( + public useGetMangaChapters( document: DocumentNode | TypedDocumentNode, mangaId: number | string, options?: QueryHookOptions, diff --git a/src/modules/chapter/Chapter.types.ts b/src/modules/chapter/Chapter.types.ts index 73beec61..111bdb7b 100644 --- a/src/modules/chapter/Chapter.types.ts +++ b/src/modules/chapter/Chapter.types.ts @@ -7,6 +7,7 @@ */ import { NullAndUndefined } from '@/Base.types.ts'; +import { ChapterReaderFieldsFragment } from '@/lib/graphql/generated/graphql.ts'; export type ChapterSortMode = 'fetchedAt' | 'source' | 'chapterNumber' | 'uploadedAt'; @@ -25,3 +26,5 @@ export type ChapterOptionsReducerAction = | { type: 'sortBy'; sortBy: ChapterSortMode } | { type: 'sortReverse' } | { type: 'showChapterNumber' }; + +export type TChapterReader = ChapterReaderFieldsFragment; diff --git a/src/modules/chapter/services/Chapters.ts b/src/modules/chapter/services/Chapters.ts index cab1116e..795eb04a 100644 --- a/src/modules/chapter/services/Chapters.ts +++ b/src/modules/chapter/services/Chapters.ts @@ -92,6 +92,7 @@ export type ChapterDownloadInfo = ChapterIdInfo & Pick; export type ChapterReadInfo = ChapterIdInfo & Pick; export type ChapterNumberInfo = ChapterIdInfo & Pick; +export type ChapterSourceOrderInfo = ChapterIdInfo & Pick; export type ChapterScanlatorInfo = ChapterIdInfo & Pick; export type ChapterRealUrlInfo = Pick; @@ -115,6 +116,10 @@ export class Chapters { }); } + static getReaderUrl(chapter: Chapter): string { + return `manga/${chapter.mangaId}/chapter/${chapter.sourceOrder}`; + } + static isDownloading(id: number): boolean { return !!requestManager.graphQLClient.client.cache.readFragment({ id: requestManager.graphQLClient.client.cache.identify({ diff --git a/src/modules/core/contexts/AppContext.tsx b/src/modules/core/contexts/AppContext.tsx index 30ebbed1..17fab536 100644 --- a/src/modules/core/contexts/AppContext.tsx +++ b/src/modules/core/contexts/AppContext.tsx @@ -12,10 +12,7 @@ import { BrowserRouter as Router } from 'react-router-dom'; import { QueryParamProvider } from 'use-query-params'; import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6'; import { useTranslation } from 'react-i18next'; -import { CacheProvider, EmotionCache } from '@emotion/react'; -import createCache from '@emotion/cache'; -import { prefixer } from 'stylis'; -import rtlPlugin from 'stylis-plugin-rtl'; +import { CacheProvider } from '@emotion/react'; import { SnackbarProvider } from 'notistack'; import { createAndSetTheme } from '@/theme.tsx'; import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx'; @@ -27,21 +24,12 @@ import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; import { AppThemes, getTheme } from '@/modules/theme/services/AppThemes.ts'; import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts'; import { ReaderContextProvider } from '@/modules/reader/contexts/ReaderContextProvider.tsx'; +import { DIRECTION_TO_CACHE } from '@/modules/theme/ThemeDirectionCache.ts'; interface Props { children: React.ReactNode; } -const directionToCache: Record = { - ltr: createCache({ - key: 'muiltr', - }), - rtl: createCache({ - key: 'muirtl', - stylisPlugins: [prefixer, rtlPlugin], - }), -}; - export const AppContext: React.FC = ({ children }) => { const directionRef = useRef('ltr'); const { i18n } = useTranslation(); @@ -88,7 +76,7 @@ export const AppContext: React.FC = ({ children }) => { return ( - + diff --git a/src/modules/core/utils/ApplyStyles.ts b/src/modules/core/utils/ApplyStyles.ts new file mode 100644 index 00000000..12fb13e7 --- /dev/null +++ b/src/modules/core/utils/ApplyStyles.ts @@ -0,0 +1,15 @@ +/* + * 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 { Theme } from '@mui/material/styles'; + +// use CSSObject instead of SxProps because this completely fucks over typescript by causing an out of memory error during compilation +type CSSObject = ReturnType; + +const emptyStyle = {}; +export const applyStyles = (isActive: boolean, styling: CSSObject): CSSObject => (isActive ? styling : emptyStyle); diff --git a/src/modules/core/utils/MediaQuery.tsx b/src/modules/core/utils/MediaQuery.tsx index f481b03d..ceedc673 100644 --- a/src/modules/core/utils/MediaQuery.tsx +++ b/src/modules/core/utils/MediaQuery.tsx @@ -27,18 +27,45 @@ export class MediaQuery { return this.useIsBelowWidth('sm'); } - static useGetScrollbarSize(type: 'height' | 'width'): number { + private static getScrollbarSize(type: 'height' | 'width'): number { + const outer = document.createElement('div'); + outer.style.visibility = 'hidden'; + outer.style.overflow = 'scroll'; + document.body.appendChild(outer); + + const inner = document.createElement('div'); + inner.style.width = '100%'; + inner.style.height = '100%'; + outer.appendChild(inner); + + const width = outer.offsetWidth - inner.offsetWidth; + const height = outer.offsetHeight - inner.offsetHeight; + + outer.parentNode?.removeChild(outer); + + return type === 'height' ? height : width; + } + + static useGetScrollbarSize( + type: 'height' | 'width', + element: HTMLElement | null = document.documentElement, + ): number { const [scrollbarSize, setScrollbarSize] = useState(0); useResizeObserver( - document.documentElement, + element, useCallback(() => { - const height = window.innerHeight - document.documentElement.clientHeight; - const width = window.innerWidth - document.documentElement.clientWidth; - const size = type === 'height' ? height : width; + const hasYScrollbar = !!(element!.scrollHeight - element!.clientHeight); + const hasXScrollbar = !!(element!.scrollWidth - element!.clientWidth); - setScrollbarSize(size); - }, []), + const hasScrollbar = (type === 'height' && hasYScrollbar) || (type === 'width' && hasXScrollbar); + if (hasScrollbar) { + setScrollbarSize(this.getScrollbarSize(type)); + return; + } + + setScrollbarSize(0); + }, [element]), ); return scrollbarSize; diff --git a/src/modules/metadata/Metadata.constants.ts b/src/modules/metadata/Metadata.constants.ts index c80e20c4..cd410c49 100644 --- a/src/modules/metadata/Metadata.constants.ts +++ b/src/modules/metadata/Metadata.constants.ts @@ -57,6 +57,9 @@ const APP_METADATA_OBJECT: Record = { tapZoneLayout: undefined, tapZoneInvertMode: undefined, readingDirection: undefined, + progressBarType: undefined, + progressBarSize: undefined, + progressBarPosition: undefined, }; export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA_OBJECT); diff --git a/src/modules/reader/components/overlay/progress-bar/ProgressBarHighlightReadPages.tsx b/src/modules/reader/components/overlay/progress-bar/ProgressBarHighlightReadPages.tsx new file mode 100644 index 00000000..d7f1fe17 --- /dev/null +++ b/src/modules/reader/components/overlay/progress-bar/ProgressBarHighlightReadPages.tsx @@ -0,0 +1,39 @@ +/* + * 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 Box from '@mui/material/Box'; +import { styled } from '@mui/material/styles'; +import { shouldForwardProp } from '@/modules/core/utils/ShouldForwardProp.ts'; +import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; +import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts'; +import { getProgressBarPositionInfo } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx'; + +type ProgressBarHighlightReadPagesProps = Pick & { + currentPagesIndex: number; + pagesLength: number; +}; +export const ProgressBarHighlightReadPages = styled(Box, { + shouldForwardProp: shouldForwardProp([ + 'currentPagesIndex', + 'pagesLength', + 'progressBarPosition', + ]), +})(({ currentPagesIndex, pagesLength, progressBarPosition }) => ({ + position: 'absolute', + pointerEvents: 'none', + ...applyStyles(getProgressBarPositionInfo(progressBarPosition).isHorizontal, { + left: 0, + width: `${((currentPagesIndex + 1) / pagesLength) * 100}%`, + height: '100%', + }), + ...applyStyles(getProgressBarPositionInfo(progressBarPosition).isVertical, { + top: 0, + width: '100%', + height: `${((currentPagesIndex + 1) / pagesLength) * 100}%`, + }), +})); diff --git a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBar.tsx b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBar.tsx new file mode 100644 index 00000000..fb0fbfe7 --- /dev/null +++ b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBar.tsx @@ -0,0 +1,205 @@ +/* + * 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 Box, { BoxProps } from '@mui/material/Box'; +import { ReactNode, useCallback, useMemo, useRef } from 'react'; +import ClickAwayListener from '@mui/material/ClickAwayListener'; +import { TypographyProps } from '@mui/material/Typography'; +import { StackProps } from '@mui/material/Stack'; +import { useReaderProgressBarContext } from '@/modules/reader/contexts/ReaderProgressBarContext.tsx'; +import { ReaderProgressBarProps } from '@/modules/reader/types/ReaderProgressBar.types.ts'; +import { ReaderProgressBarPageNumber } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBarPageNumber.tsx'; +import { ReaderProgressBarContainer } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBarContainer.tsx'; +import { ReaderProgressBarRoot } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBarRoot.tsx'; +import { ReaderProgressBarSlotsContainer } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlotsContainer.tsx'; +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, + useHandleProgressDragging, +} from '@/modules/reader/utils/ReaderProgressBar.utils.tsx'; +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'; + +export const ReaderProgressBar = ({ + totalPages, + pages, + pageLoadStates, + currentPageIndex, + setCurrentPageIndex, + slotProps, + slots, + createProgressBarSlot, + progressBarPosition, +}: ReaderProgressBarProps & { + createProgressBarSlot: ( + page: ReaderProgressBarProps['pages'][number], + pageLoadStates: ReaderProgressBarProps['pageLoadStates'], + pagesIndex: number, + ) => ReactNode; + slotProps?: { + container?: StackProps; + progressBarRoot?: StackProps; + progressBarSlotsActionArea?: StackProps; + progressBarSlotsContainer?: StackProps; + progressBarSlot?: BoxProps; + progressBarReadPages?: BoxProps; + progressBarCurrentPageSlot?: BoxProps; + progressBarPageTexts?: { + base?: TypographyProps; + current?: TypographyProps; + total?: TypographyProps; + }; + }; + slots?: { + progressBarCurrentPage?: ReactNode; + }; +}) => { + const { isDragging, setIsDragging } = useReaderProgressBarContext(); + + 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), + [direction], + ); + + useHandleProgressDragging( + progressBarRef, + isDragging, + currentPage, + setCurrentPageIndex, + pages, + progressBarPosition, + getOptionForDirection, + ); + + return ( + + + setCurrentPageIndex(getNextPageIndex('previous', currentPage.pagesIndex, pages))} + > + {currentPage.name} + + setIsDragging(false)}> + setIsDragging(false)} + onTouchStart={(event) => { + if (!progressBarRef.current) { + return; + } + + setCurrentPageIndex( + getPageForMousePos( + event.touches[0], + progressBarRef.current.getBoundingClientRect(), + pages, + isHorizontalPosition, + getOptionForDirection, + ).primary.index, + ); + setIsDragging(true); + }} + onMouseUp={() => setIsDragging(false)} + onMouseDown={(event) => { + if (!progressBarRef.current) { + return; + } + + setCurrentPageIndex( + getPageForMousePos( + event, + progressBarRef.current.getBoundingClientRect(), + pages, + isHorizontalPosition, + getOptionForDirection, + ).primary.index, + ); + setIsDragging(true); + }} + > + + {pages.map((page, pagesIndex) => ( + + {createProgressBarSlot(page, pageLoadStates, pagesIndex)} + + ))} + + + + {slots?.progressBarCurrentPage} + + + + setCurrentPageIndex(getNextPageIndex('next', currentPage.pagesIndex, pages))} + > + {totalPages} + + + + ); +}; diff --git a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarContainer.tsx b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarContainer.tsx new file mode 100644 index 00000000..1fa7388f --- /dev/null +++ b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarContainer.tsx @@ -0,0 +1,36 @@ +/* + * 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 Stack from '@mui/material/Stack'; +import { styled } from '@mui/material/styles'; +import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; +import { shouldForwardProp } from '@/modules/core/utils/ShouldForwardProp.ts'; +import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts'; +import { getProgressBarPositionInfo } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx'; + +type ReaderProgressBarContainerProps = Pick; +export const ReaderProgressBarContainer = styled(Stack, { + shouldForwardProp: shouldForwardProp(['progressBarPosition']), +})(({ theme, progressBarPosition }) => ({ + position: 'fixed', + ...applyStyles(getProgressBarPositionInfo(progressBarPosition).isHorizontal, { + justifyContent: 'flex-end', + bottom: 0, + right: 0, + }), + ...applyStyles(getProgressBarPositionInfo(progressBarPosition).isVertical, { + top: 0, + bottom: 0, + }), + ...applyStyles(getProgressBarPositionInfo(progressBarPosition).isLeft, { + alignItems: theme.direction === 'ltr' ? 'flex-start' : 'flex-end', + }), + ...applyStyles(getProgressBarPositionInfo(progressBarPosition).isRight, { + alignItems: theme.direction === 'ltr' ? 'flex-end' : 'flex-start', + }), +})); diff --git a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarCurrentPageSlot.tsx b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarCurrentPageSlot.tsx new file mode 100644 index 00000000..adefac32 --- /dev/null +++ b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarCurrentPageSlot.tsx @@ -0,0 +1,71 @@ +/* + * 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 Box from '@mui/material/Box'; +import Tooltip from '@mui/material/Tooltip'; +import { ReactNode } from 'react'; +import { CurrentPageSlotProps } from '@/modules/reader/types/ReaderProgressBar.types.ts'; +import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts'; +import { READER_PROGRESS_BAR_POSITION_TO_PLACEMENT } from '@/modules/reader/constants/ReaderProgressBar.constants.ts'; +import { getProgressBarPositionInfo } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx'; + +export const ReaderProgressBarCurrentPageSlot = ({ + pageName, + currentPagesIndex, + pagesLength, + isDragging, + setIsDragging, + boxProps, + children, + progressBarPosition, +}: CurrentPageSlotProps & { children?: ReactNode }) => ( + + { + e.stopPropagation(); + setIsDragging(false); + }} + onTouchStart={(e) => { + e.stopPropagation(); + setIsDragging(true); + }} + onMouseUp={(e) => { + e.stopPropagation(); + setIsDragging(false); + }} + onMouseDown={(e) => { + e.stopPropagation(); + setIsDragging(true); + }} + > + {children} + + +); diff --git a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarDirectionWrapper.tsx b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarDirectionWrapper.tsx new file mode 100644 index 00000000..a7d8e2ab --- /dev/null +++ b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarDirectionWrapper.tsx @@ -0,0 +1,42 @@ +/* + * 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 { ReactNode, useMemo } from 'react'; +import { CacheProvider } from '@emotion/react'; +import { ThemeProvider } from '@mui/material/styles'; +import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts'; +import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx'; +import { AppThemes, getTheme } from '@/modules/theme/services/AppThemes.ts'; +import { ThemeMode } from '@/modules/theme/contexts/ThemeModeContext.tsx'; +import { createTheme } from '@/theme.tsx'; +import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; +import { DIRECTION_TO_CACHE } from '@/modules/theme/ThemeDirectionCache.ts'; + +export const ReaderProgressBarDirectionWrapper = ({ children }: { children: ReactNode }) => { + const direction = ReaderService.useGetThemeDirection(); + const [appTheme] = useLocalStorage('appTheme', 'default'); + const [themeMode] = useLocalStorage('themeMode', ThemeMode.SYSTEM); + const [pureBlackMode] = useLocalStorage('pureBlackMode', false); + + const { + settings: { customThemes }, + } = useMetadataServerSettings(); + + const readerTheme = useMemo( + () => createTheme(themeMode, getTheme(appTheme, customThemes), pureBlackMode, direction), + [themeMode, appTheme, customThemes, pureBlackMode, direction], + ); + + return ( + + +
{children}
+
+
+ ); +}; diff --git a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarPageNumber.tsx b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarPageNumber.tsx new file mode 100644 index 00000000..2a9fa268 --- /dev/null +++ b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarPageNumber.tsx @@ -0,0 +1,22 @@ +/* + * 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 { styled } from '@mui/material/styles'; +import Typography from '@mui/material/Typography'; + +export const ReaderProgressBarPageNumber = styled(Typography)({ + boxSizing: 'content-box', + minWidth: '45px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + overflow: 'hidden', + textAlign: 'center', + userSelect: 'none', + cursor: 'pointer', +}); diff --git a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarRoot.tsx b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarRoot.tsx new file mode 100644 index 00000000..edcadc0a --- /dev/null +++ b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarRoot.tsx @@ -0,0 +1,17 @@ +/* + * 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 { styled } from '@mui/material/styles'; +import Stack from '@mui/material/Stack'; + +export const ReaderProgressBarRoot = styled(Stack)(({ theme }) => ({ + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + gap: theme.spacing(0.5), +})); diff --git a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlot.tsx b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlot.tsx new file mode 100644 index 00000000..2e9fbde7 --- /dev/null +++ b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlot.tsx @@ -0,0 +1,31 @@ +/* + * 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 Box from '@mui/material/Box'; +import Tooltip from '@mui/material/Tooltip'; +import { ReactNode } from 'react'; +import { ReaderProgressBarSlotProps } from '@/modules/reader/types/ReaderProgressBar.types.ts'; +import { READER_PROGRESS_BAR_POSITION_TO_PLACEMENT } from '@/modules/reader/constants/ReaderProgressBar.constants.ts'; + +export const ReaderProgressBarSlot = ({ + pageName, + progressBarPosition, + slotProps, + children, +}: ReaderProgressBarSlotProps & { children?: ReactNode }) => ( + + + {children} + + +); diff --git a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlotsActionArea.tsx b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlotsActionArea.tsx new file mode 100644 index 00000000..c640f23f --- /dev/null +++ b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlotsActionArea.tsx @@ -0,0 +1,16 @@ +/* + * 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 Stack from '@mui/material/Stack'; +import { styled } from '@mui/material/styles'; + +export const ReaderProgressBarSlotsActionArea = styled(Stack)({ + position: 'relative', + flexDirection: 'row', + flexGrow: 1, +}); diff --git a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlotsContainer.tsx b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlotsContainer.tsx new file mode 100644 index 00000000..33c653db --- /dev/null +++ b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlotsContainer.tsx @@ -0,0 +1,16 @@ +/* + * 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 { styled } from '@mui/material/styles'; +import Stack from '@mui/material/Stack'; + +export const ReaderProgressBarSlotsContainer = styled(Stack)({ + flexDirection: 'row', + flexGrow: 1, + overflow: 'hidden', +}); diff --git a/src/modules/reader/components/overlay/progress-bar/variants/MobileReaderProgressBar.tsx b/src/modules/reader/components/overlay/progress-bar/variants/MobileReaderProgressBar.tsx new file mode 100644 index 00000000..36e9c270 --- /dev/null +++ b/src/modules/reader/components/overlay/progress-bar/variants/MobileReaderProgressBar.tsx @@ -0,0 +1,179 @@ +/* + * 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 IconButton from '@mui/material/IconButton'; +import SkipPreviousIcon from '@mui/icons-material/SkipPrevious'; +import SkipNextIcon from '@mui/icons-material/SkipNext'; +import Stack from '@mui/material/Stack'; +import { alpha } from '@mui/material/styles'; +import Box from '@mui/material/Box'; +import { useLayoutEffect } from 'react'; +import { ReaderProgressBar } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBar.tsx'; +import { ReaderProgressBarSlot } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlot.tsx'; +import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx'; +import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx'; +import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; +import { getPage } from '@/modules/reader/utils/ReaderProgressBar.utils'; +import { getOptionForDirection } from '@/theme.tsx'; +import { ProgressBarPosition } from '@/modules/reader/types/Reader.types.ts'; +import { ReaderProgressBarDirectionWrapper } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBarDirectionWrapper.tsx'; +import { useReaderProgressBarContext } from '@/modules/reader/contexts/ReaderProgressBarContext.tsx'; +import { useReaderOverlayContext } from '@/modules/reader/contexts/ReaderOverlayContext.tsx'; + +export const MobileReaderProgressBar = () => { + const { nextChapter, previousChapter } = useReaderStateChaptersContext(); + const { isVisible } = useReaderOverlayContext(); + const { setIsMaximized } = useReaderProgressBarContext(); + const pagesState = userReaderStatePagesContext(); + const { currentPageIndex, pages } = pagesState; + + const direction = ReaderService.useGetThemeDirection(); + + const openNextChapter = ReaderService.useNavigateToChapter(nextChapter); + const openPreviousChapter = ReaderService.useNavigateToChapter(previousChapter); + + useLayoutEffect(() => { + setIsMaximized(isVisible); + + return () => setIsMaximized(false); + }, [isVisible]); + + return ( + + + alpha(theme.palette.background.paper, 0.85) }} + > + {getOptionForDirection(, , direction)} + + ( + + + + )} + slotProps={{ + container: { + sx: { + flexGrow: 1, + position: 'relative', + display: 'flex', + justifyItems: 'center', + alignItems: 'stretch', + backgroundColor: (theme) => alpha(theme.palette.background.paper, 0.85), + borderRadius: 100, + }, + }, + progressBarRoot: { + sx: { + flexGrow: 1, + alignItems: 'stretch', + gap: 0, + }, + }, + progressBarSlotsActionArea: { + sx: { + height: '100%', + alignItems: 'center', + py: 2, + cursor: 'pointer', + }, + }, + + progressBarSlotsContainer: { + sx: { + borderRadius: 100, + }, + }, + + progressBarSlot: { + sx: { + height: '20px', + }, + }, + progressBarReadPages: { + sx: { + height: '20px', + backgroundColor: 'primary.main', + borderRadius: '400px 0 0 400px', + width: `calc(${(Math.max(0, getPage(currentPageIndex, pages).pagesIndex) / pages.length) * 100}% + 100% / ${pages.length})`, + }, + }, + progressBarCurrentPageSlot: { + sx: { + display: 'flex', + justifyContent: 'end', + alignItems: 'center', + zIndex: 1, + pointer: 'default', + }, + }, + progressBarPageTexts: { + base: { px: 1 }, + }, + }} + slots={{ + progressBarCurrentPage: ( + + ), + }} + /> + alpha(theme.palette.background.paper, 0.85) }} + > + {getOptionForDirection(, , direction)} + + + + ); +}; diff --git a/src/modules/reader/components/overlay/progress-bar/variants/StandardReaderProgressBar.tsx b/src/modules/reader/components/overlay/progress-bar/variants/StandardReaderProgressBar.tsx new file mode 100644 index 00000000..1d23b352 --- /dev/null +++ b/src/modules/reader/components/overlay/progress-bar/variants/StandardReaderProgressBar.tsx @@ -0,0 +1,257 @@ +/* + * 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 { alpha, darken, lighten, useTheme } from '@mui/material/styles'; +import Box from '@mui/material/Box'; +import { ReaderProgressBar } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBar.tsx'; +import { useReaderProgressBarContext } from '@/modules/reader/contexts/ReaderProgressBarContext.tsx'; +import { ReaderProgressBarSlot } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlot.tsx'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; +import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx'; +import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; +import { ProgressBarType } from '@/modules/reader/types/Reader.types.ts'; +import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts'; +import { getPage, getProgressBarPositionInfo } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx'; +import { ReaderProgressBarDirectionWrapper } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBarDirectionWrapper.tsx'; +import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx'; + +export const StandardReaderProgressBar = () => { + const theme = useTheme(); + const pagesState = userReaderStatePagesContext(); + const { currentPageIndex, pages } = pagesState; + const { readerNavBarWidth } = useNavBarContext(); + const { isMaximized, setIsMaximized, isDragging } = useReaderProgressBarContext(); + const { progressBarType, progressBarSize, progressBarPosition } = ReaderService.useSettings(); + const readerDirection = ReaderService.useGetThemeDirection(); + const { scrollbarXSize, scrollbarYSize } = useReaderScrollbarContext(); + + const currentPagesIndex = getPage(currentPageIndex, pages).pagesIndex; + + const { isBottom, isLeft, isRight, isVertical, isHorizontal } = getProgressBarPositionInfo(progressBarPosition); + + const isHidden = progressBarType === ProgressBarType.HIDDEN; + const isMinimized = !isMaximized && !isDragging; + + // the progress bar uses the reading direction to set the themes direction, thus, stuff has to be adjusted to still be correctly positioned + // depending on which combination of theme direction and reading direction is currently active + return ( + + ( + + + + )} + slotProps={{ + container: { + sx: { + overflow: 'hidden', + transition: `all 0.${theme.transitions.duration.shortest}s`, + ...applyStyles(isHorizontal, { + minHeight: '100px', + ...applyStyles(theme.direction === 'ltr', { + left: readerDirection === 'ltr' ? readerNavBarWidth : scrollbarYSize, + right: readerDirection === 'rtl' ? readerNavBarWidth : scrollbarYSize, + }), + ...applyStyles(theme.direction === 'rtl', { + left: readerDirection === 'rtl' ? readerNavBarWidth : scrollbarYSize, + right: readerDirection === 'ltr' ? readerNavBarWidth : scrollbarYSize, + }), + }), + ...applyStyles(isVertical, { + minWidth: '100px', + bottom: `${scrollbarXSize}px`, + }), + ...applyStyles(isBottom, { + bottom: `${scrollbarXSize}px`, + }), + ...applyStyles(isLeft, { + ...applyStyles(theme.direction === 'ltr', { + left: readerDirection === 'ltr' ? readerNavBarWidth : 'unset', + right: readerDirection === 'rtl' ? readerNavBarWidth : 'unset', + }), + ...applyStyles(theme.direction === 'rtl', { + right: readerDirection === 'rtl' ? scrollbarYSize : 'unset', + left: readerDirection === 'ltr' ? scrollbarYSize : 'unset', + }), + }), + ...applyStyles(isRight, { + ...applyStyles(theme.direction === 'ltr', { + right: readerDirection === 'ltr' ? scrollbarYSize : 'unset', + left: readerDirection === 'rtl' ? scrollbarYSize : 'unset', + }), + ...applyStyles(theme.direction === 'rtl', { + left: readerDirection === 'rtl' ? readerNavBarWidth : 'unset', + right: readerDirection === 'ltr' ? readerNavBarWidth : 'unset', + }), + }), + ...applyStyles(isMinimized, { + opacity: !isHidden ? 0.85 : 0, + }), + }, + onMouseEnter: () => setIsMaximized(true), + onMouseLeave: () => setIsMaximized(false), + }, + progressBarRoot: { + sx: { + gap: 0, + transition: 'all 0.1s ease-in-out', + backgroundColor: 'background.paper', + ...applyStyles(isVertical, { + flexDirection: 'column', + height: '100%', + }), + ...applyStyles(isMinimized, { + backgroundColor: 'unset', + pointerEvents: 'none', + ...applyStyles(isHorizontal, { + px: 2, + }), + ...applyStyles(isVertical, { + py: 2, + }), + ...applyStyles(isBottom, { + pb: 0.25, + }), + ...applyStyles(isLeft, { + ...applyStyles(theme.direction === 'ltr', { + pl: readerDirection === 'ltr' ? 0.25 : 0, + pr: readerDirection === 'rtl' ? 0.25 : 0, + }), + ...applyStyles(theme.direction === 'rtl', { + pr: readerDirection === 'rtl' ? 0.25 : 0, + pl: readerDirection === 'ltr' ? 0.25 : 0, + }), + }), + ...applyStyles(isRight, { + ...applyStyles(theme.direction === 'ltr', { + pr: readerDirection === 'ltr' ? 0.25 : 0, + pl: readerDirection === 'rtl' ? 0.25 : 0, + }), + ...applyStyles(theme.direction === 'rtl', { + pl: readerDirection === 'rtl' ? 0.25 : 0, + pr: readerDirection === 'ltr' ? 0.25 : 0, + }), + }), + }), + }, + }, + progressBarSlotsContainer: { + sx: { + borderRadius: 2, + transition: 'height 0.1s ease-in-out', + ...applyStyles(isHorizontal, { + height: '20px', + }), + ...applyStyles(isVertical, { + flexDirection: 'column', + width: '20px', + }), + ...applyStyles(isMinimized, { + ...applyStyles(isHorizontal, { + height: `${progressBarSize}px`, + }), + ...applyStyles(isVertical, { + width: `${progressBarSize}px`, + }), + }), + }, + }, + progressBarReadPages: { sx: { display: 'none' } }, + progressBarCurrentPageSlot: { sx: { display: 'none' } }, + progressBarPageTexts: { + base: { + sx: { + px: 1, + py: 1.5, + transition: 'all 0.1s ease-in-out', + ...applyStyles(isMinimized, { + transform: 'scale(0)', + p: 0, + minWidth: 0, + maxWidth: 0, + minHeight: 0, + maxHeight: 0, + }), + }, + }, + }, + }} + /> + + ); +}; diff --git a/src/modules/reader/constants/Reader.constants.ts b/src/modules/reader/constants/Reader.constants.ts index ee02054e..3550c078 100644 --- a/src/modules/reader/constants/Reader.constants.ts +++ b/src/modules/reader/constants/Reader.constants.ts @@ -6,7 +6,12 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { IReaderSettings, ReadingDirection } from '@/modules/reader/types/Reader.types.ts'; +import { + IReaderSettings, + ProgressBarPosition, + ProgressBarType, + ReadingDirection, +} from '@/modules/reader/types/Reader.types.ts'; import { TapZoneLayouts } from '@/modules/reader/types/TapZoneLayout.types.ts'; export const DEFAULT_READER_SETTINGS: IReaderSettings = { @@ -20,5 +25,8 @@ export const DEFAULT_READER_SETTINGS: IReaderSettings = { readerWidth: 50, tapZoneLayout: TapZoneLayouts.RIGHT_LEFT, tapZoneInvertMode: { vertical: false, horizontal: false }, + progressBarType: ProgressBarType.STANDARD, + progressBarSize: 4, + progressBarPosition: ProgressBarPosition.BOTTOM, readingDirection: ReadingDirection.LTR, }; diff --git a/src/modules/reader/constants/ReaderProgressBar.constants.ts b/src/modules/reader/constants/ReaderProgressBar.constants.ts new file mode 100644 index 00000000..dedb392a --- /dev/null +++ b/src/modules/reader/constants/ReaderProgressBar.constants.ts @@ -0,0 +1,16 @@ +/* + * 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 { TooltipProps } from '@mui/material/Tooltip'; +import { ProgressBarPosition } from '@/modules/reader/types/Reader.types.ts'; + +export const READER_PROGRESS_BAR_POSITION_TO_PLACEMENT: Record = { + [ProgressBarPosition.BOTTOM]: 'top', + [ProgressBarPosition.LEFT]: 'right', + [ProgressBarPosition.RIGHT]: 'left', +}; diff --git a/src/modules/reader/contexts/ReaderContextProvider.tsx b/src/modules/reader/contexts/ReaderContextProvider.tsx index 2d146bf6..b758c5fe 100644 --- a/src/modules/reader/contexts/ReaderContextProvider.tsx +++ b/src/modules/reader/contexts/ReaderContextProvider.tsx @@ -8,7 +8,19 @@ import { ReactNode } from 'react'; import { ReaderTapZoneContextProvider } from '@/modules/reader/contexts/ReaderTapZoneContextProvider.tsx'; +import { ReaderProgressBarContextProvider } from '@/modules/reader/contexts/ReaderProgressBarContextProvider.tsx'; +import { ReaderOverlayContextProvider } from '@/modules/reader/contexts/ReaderOverlayContextProvider.tsx'; +import { ReaderStateContextProvider } from '@/modules/reader/contexts/state/ReaderStateContextProvider.tsx'; +import { ReaderScrollbarContextProvider } from '@/modules/reader/contexts/ReaderScrollbarContextProvider.tsx'; export const ReaderContextProvider = ({ children }: { children?: ReactNode }) => ( - {children} + + + + + {children} + + + + ); diff --git a/src/modules/reader/contexts/ReaderOverlayContext.tsx b/src/modules/reader/contexts/ReaderOverlayContext.tsx new file mode 100644 index 00000000..9b2aba86 --- /dev/null +++ b/src/modules/reader/contexts/ReaderOverlayContext.tsx @@ -0,0 +1,21 @@ +/* + * 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 React, { createContext, useContext } from 'react'; + +type TReaderOverlayContext = { + isVisible: boolean; + setIsVisible: React.Dispatch>; +}; + +export const ReaderOverlayContext = createContext({ + isVisible: false, + setIsVisible: () => {}, +}); + +export const useReaderOverlayContext = () => useContext(ReaderOverlayContext); diff --git a/src/modules/reader/contexts/ReaderOverlayContextProvider.tsx b/src/modules/reader/contexts/ReaderOverlayContextProvider.tsx new file mode 100644 index 00000000..454e3b90 --- /dev/null +++ b/src/modules/reader/contexts/ReaderOverlayContextProvider.tsx @@ -0,0 +1,18 @@ +/* + * 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 { ReactNode, useMemo, useState } from 'react'; +import { ReaderOverlayContext } from '@/modules/reader/contexts/ReaderOverlayContext.tsx'; + +export const ReaderOverlayContextProvider = ({ children }: { children: ReactNode }) => { + const [isVisible, setIsVisible] = useState(false); + + const value = useMemo(() => ({ isVisible, setIsVisible }), [isVisible]); + + return {children}; +}; diff --git a/src/modules/reader/contexts/ReaderProgressBarContext.tsx b/src/modules/reader/contexts/ReaderProgressBarContext.tsx new file mode 100644 index 00000000..5379ef7e --- /dev/null +++ b/src/modules/reader/contexts/ReaderProgressBarContext.tsx @@ -0,0 +1,25 @@ +/* + * 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 { createContext, useContext } from 'react'; + +type TReaderContext = { + isMaximized: boolean; + setIsMaximized: (visible: boolean) => void; + isDragging: boolean; + setIsDragging: (isDragging: boolean) => void; +}; + +export const ReaderProgressBarContext = createContext({ + isMaximized: false, + setIsMaximized: () => undefined, + isDragging: false, + setIsDragging: () => undefined, +}); + +export const useReaderProgressBarContext = () => useContext(ReaderProgressBarContext); diff --git a/src/modules/reader/contexts/ReaderProgressBarContextProvider.tsx b/src/modules/reader/contexts/ReaderProgressBarContextProvider.tsx new file mode 100644 index 00000000..178935f2 --- /dev/null +++ b/src/modules/reader/contexts/ReaderProgressBarContextProvider.tsx @@ -0,0 +1,22 @@ +/* + * 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 { ReactNode, useMemo, useState } from 'react'; +import { ReaderProgressBarContext } from '@/modules/reader/contexts/ReaderProgressBarContext.tsx'; + +export const ReaderProgressBarContextProvider = ({ children }: { children: ReactNode }) => { + const [isMaximized, setIsMaximized] = useState(false); + const [isDragging, setIsDragging] = useState(false); + + const value = useMemo( + () => ({ isMaximized, setIsMaximized, isDragging, setIsDragging }), + [isMaximized, isDragging], + ); + + return {children}; +}; diff --git a/src/modules/reader/contexts/ReaderScrollbarContext.tsx b/src/modules/reader/contexts/ReaderScrollbarContext.tsx new file mode 100644 index 00000000..1b0ae065 --- /dev/null +++ b/src/modules/reader/contexts/ReaderScrollbarContext.tsx @@ -0,0 +1,25 @@ +/* + * 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 { createContext, useContext } from 'react'; + +type TReaderScrollbarContext = { + scrollbarXSize: number; + setScrollbarXSize: (size: number) => void; + scrollbarYSize: number; + setScrollbarYSize: (size: number) => void; +}; + +export const ReaderScrollbarContext = createContext({ + scrollbarXSize: 0, + setScrollbarXSize: () => undefined, + scrollbarYSize: 0, + setScrollbarYSize: () => undefined, +}); + +export const useReaderScrollbarContext = () => useContext(ReaderScrollbarContext); diff --git a/src/modules/reader/contexts/ReaderScrollbarContextProvider.tsx b/src/modules/reader/contexts/ReaderScrollbarContextProvider.tsx new file mode 100644 index 00000000..75f37248 --- /dev/null +++ b/src/modules/reader/contexts/ReaderScrollbarContextProvider.tsx @@ -0,0 +1,22 @@ +/* + * 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 { ReactNode, useMemo, useState } from 'react'; +import { ReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx'; + +export const ReaderScrollbarContextProvider = ({ children }: { children: ReactNode }) => { + const [scrollbarXSize, setScrollbarXSize] = useState(0); + const [scrollbarYSize, setScrollbarYSize] = useState(0); + + const value = useMemo( + () => ({ scrollbarXSize, setScrollbarXSize, scrollbarYSize, setScrollbarYSize }), + [scrollbarXSize, scrollbarYSize], + ); + + return {children}; +}; diff --git a/src/modules/reader/contexts/state/ReaderStateChaptersContext.tsx b/src/modules/reader/contexts/state/ReaderStateChaptersContext.tsx new file mode 100644 index 00000000..a1042caa --- /dev/null +++ b/src/modules/reader/contexts/state/ReaderStateChaptersContext.tsx @@ -0,0 +1,17 @@ +/* + * 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 { createContext, useContext } from 'react'; +import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts'; + +export const ReaderStateChaptersContext = createContext({ + chapters: [], + setReaderStateChapters: () => {}, +}); + +export const useReaderStateChaptersContext = () => useContext(ReaderStateChaptersContext); diff --git a/src/modules/reader/contexts/state/ReaderStateChaptersContextProvider.tsx b/src/modules/reader/contexts/state/ReaderStateChaptersContextProvider.tsx new file mode 100644 index 00000000..8e435ee4 --- /dev/null +++ b/src/modules/reader/contexts/state/ReaderStateChaptersContextProvider.tsx @@ -0,0 +1,26 @@ +/* + * 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 { ContextType, ReactNode, useMemo, useState } from 'react'; +import { ReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx'; + +type TContext = ContextType; + +export const ReaderStateChaptersContextProvider = ({ children }: { children: ReactNode }) => { + const [state, setState] = useState>({ chapters: [] }); + + const value = useMemo( + () => ({ + ...state, + setReaderStateChapters: setState, + }), + [state], + ); + + return {children}; +}; diff --git a/src/modules/reader/contexts/state/ReaderStateContextProvider.tsx b/src/modules/reader/contexts/state/ReaderStateContextProvider.tsx index 4cb3bd83..12afc483 100644 --- a/src/modules/reader/contexts/state/ReaderStateContextProvider.tsx +++ b/src/modules/reader/contexts/state/ReaderStateContextProvider.tsx @@ -7,8 +7,14 @@ */ import { ReactNode } from 'react'; +import { ReaderStatePagesContextProvider } from '@/modules/reader/contexts/state/ReaderStatePagesContextProvider.tsx'; +import { ReaderStateChaptersContextProvider } from '@/modules/reader/contexts/state/ReaderStateChaptersContextProvider.tsx'; import { ReaderStateMangaContextProvider } from '@/modules/reader/contexts/state/ReaderStateMangaContextProvider.tsx'; export const ReaderStateContextProvider = ({ children }: { children: ReactNode }) => ( - {children} + + + {children} + + ); diff --git a/src/modules/reader/contexts/state/ReaderStatePagesContext.tsx b/src/modules/reader/contexts/state/ReaderStatePagesContext.tsx new file mode 100644 index 00000000..d2bc8f50 --- /dev/null +++ b/src/modules/reader/contexts/state/ReaderStatePagesContext.tsx @@ -0,0 +1,25 @@ +/* + * 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 { createContext, useContext } from 'react'; +import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts'; + +export const ReaderStatePagesContext = createContext({ + totalPages: 0, + currentPageIndex: 0, + setCurrentPageIndex: () => undefined, + setTotalPages: () => undefined, + pageUrls: [], + setPageUrls: () => undefined, + pageLoadStates: [], + setPageLoadStates: () => undefined, + pages: [], + setPages: () => undefined, +}); + +export const userReaderStatePagesContext = () => useContext(ReaderStatePagesContext); diff --git a/src/modules/reader/contexts/state/ReaderStatePagesContextProvider.tsx b/src/modules/reader/contexts/state/ReaderStatePagesContextProvider.tsx new file mode 100644 index 00000000..cf89818a --- /dev/null +++ b/src/modules/reader/contexts/state/ReaderStatePagesContextProvider.tsx @@ -0,0 +1,39 @@ +/* + * 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 { ContextType, ReactNode, useMemo, useState } from 'react'; +import { ReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx'; +import { createPageData } from '@/modules/reader/utils/ReaderPager.utils.tsx'; + +type TContext = ContextType; + +export const ReaderStatePagesContextProvider = ({ children }: { children: ReactNode }) => { + const [totalPages, setTotalPages] = useState(0); + const [currentPageIndex, setCurrentPageIndex] = useState(0); + const [pageUrls, setPageUrls] = useState([]); + const [pageLoadStates, setPageLoadStates] = useState([]); + const [pages, setPages] = useState([createPageData('', 0)]); + + const value = useMemo( + () => ({ + totalPages, + setTotalPages, + currentPageIndex, + setCurrentPageIndex, + pageUrls, + setPageUrls, + pageLoadStates, + setPageLoadStates, + pages, + setPages, + }), + [totalPages, pages, currentPageIndex, pageUrls, pageLoadStates], + ); + + return {children}; +}; diff --git a/src/modules/reader/services/ReaderService.ts b/src/modules/reader/services/ReaderService.ts index dac12493..d9ca542d 100644 --- a/src/modules/reader/services/ReaderService.ts +++ b/src/modules/reader/services/ReaderService.ts @@ -6,16 +6,47 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useMemo } from 'react'; -import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; +import { useCallback, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Direction, useTheme } from '@mui/material/styles'; +import { TChapterReader } from '@/modules/chapter/Chapter.types.ts'; +import { Chapters } from '@/modules/chapter/services/Chapters.ts'; +import { IReaderSettings, ReadingDirection } from '@/modules/reader/types/Reader.types.ts'; import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx'; import { getReaderSettingsFor, useDefaultReaderSettings } from '@/modules/reader/services/ReaderSettingsMetadata.ts'; +const DIRECTION_TO_INVERTED: Record = { + ltr: 'rtl', + rtl: 'ltr', +}; + +const DIRECTION_TO_READING_DIRECTION: Record = { + ltr: ReadingDirection.LTR, + rtl: ReadingDirection.RTL, +}; + export class ReaderService { + static useNavigateToChapter(chapter?: TChapterReader): () => void { + const navigate = useNavigate(); + + return useCallback(() => chapter && navigate(Chapters.getReaderUrl(chapter), { replace: true }), [chapter]); + } + static useSettings(): IReaderSettings { const { manga } = useReaderStateMangaContext(); - const { settings } = useDefaultReaderSettings(); + const defaultReaderSettings = useDefaultReaderSettings(); + return useMemo( + () => getReaderSettingsFor(manga ?? { id: -1 }, defaultReaderSettings.settings), + [manga, defaultReaderSettings], + ); + } - return useMemo(() => getReaderSettingsFor(manga ?? { id: -1 }, settings), [manga, settings]); + static useGetThemeDirection(): Direction { + const { direction } = useTheme(); + const { readingDirection } = ReaderService.useSettings(); + + return DIRECTION_TO_READING_DIRECTION[direction] === readingDirection + ? direction + : DIRECTION_TO_INVERTED[direction]; } } diff --git a/src/modules/reader/services/ReaderSettingsMetadata.ts b/src/modules/reader/services/ReaderSettingsMetadata.ts index 1b616073..5f07d98b 100644 --- a/src/modules/reader/services/ReaderSettingsMetadata.ts +++ b/src/modules/reader/services/ReaderSettingsMetadata.ts @@ -8,6 +8,8 @@ import { useEffect, useMemo } from 'react'; import { requestManager } from '@/lib/requests/RequestManager.ts'; +import { requestUpdateMangaMetadata } from '@/modules/metadata/services/MetadataUpdater.ts'; +import { MangaType } from '@/lib/graphql/generated/graphql.ts'; import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/Reader.constants.ts'; import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; import { convertFromGqlMeta } from '@/modules/metadata/services/MetadataConverter.ts'; @@ -20,6 +22,7 @@ import { MetadataHolder, MetadataHolderType, } from '@/modules/metadata/Metadata.types.ts'; +import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { jsonSaveParse } from '@/lib/HelperFunctions.ts'; import { MangaIdInfo } from '@/modules/manga/Manga.types.ts'; @@ -121,3 +124,18 @@ export const useDefaultReaderSettings = (): { [metadata, settings, loading, request], ); }; + +export const updateReaderSettings = async ( + manga: Pick & GqlMetaHolder, + setting: Setting, + value: IReaderSettings[Setting], +): Promise => + requestUpdateMangaMetadata(manga, [[setting, convertSettingsToMetadata({ [setting]: value })[setting]]]); + +export const createUpdateReaderSettings = + ( + manga: Pick & GqlMetaHolder, + handleError: (error: any) => void = defaultPromiseErrorHandler('createUpdateReaderSettings'), + ): ((...args: OmitFirst>>) => Promise) => + (setting, value) => + updateReaderSettings(manga, setting, value).catch(handleError); diff --git a/src/modules/reader/types/Reader.types.ts b/src/modules/reader/types/Reader.types.ts index 63b22075..82953e5b 100644 --- a/src/modules/reader/types/Reader.types.ts +++ b/src/modules/reader/types/Reader.types.ts @@ -7,12 +7,32 @@ */ import { TapZoneInvertMode, TapZoneLayouts } from '@/modules/reader/types/TapZoneLayout.types.ts'; +import { TChapterReader } from '@/modules/chapter/Chapter.types.ts'; + +export enum ProgressBarType { + HIDDEN, + STANDARD, +} + +export enum ProgressBarPosition { + BOTTOM, + LEFT, + RIGHT, +} export enum ReadingDirection { LTR, RTL, } +export interface ReaderStateChapters { + chapters: TChapterReader[]; + currentChapter?: TChapterReader | null; + nextChapter?: TChapterReader; + previousChapter?: TChapterReader; + setReaderStateChapters: React.Dispatch>>; +} + export interface IReaderSettings { staticNav: boolean; showPageNumber: boolean; @@ -24,5 +44,8 @@ export interface IReaderSettings { readerWidth: number; tapZoneLayout: TapZoneLayouts; tapZoneInvertMode: TapZoneInvertMode; + progressBarType: ProgressBarType; + progressBarSize: number; + progressBarPosition: ProgressBarPosition; readingDirection: ReadingDirection; } diff --git a/src/modules/reader/types/ReaderProgressBar.types.ts b/src/modules/reader/types/ReaderProgressBar.types.ts new file mode 100644 index 00000000..0ea96bad --- /dev/null +++ b/src/modules/reader/types/ReaderProgressBar.types.ts @@ -0,0 +1,61 @@ +/* + * 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 { BoxProps } from '@mui/material/Box'; +import { TooltipProps } from '@mui/material/Tooltip'; +import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; + +interface SinglePageData { + index: number; + alt: string; + url: string; +} + +interface PageData { + name: string; + primary: SinglePageData; + secondary?: SinglePageData; +} + +export interface ReaderStatePages { + totalPages: number; + setTotalPages: React.Dispatch>; + currentPageIndex: number; + setCurrentPageIndex: React.Dispatch>; + pageUrls: string[]; + setPageUrls: React.Dispatch>; + pageLoadStates: boolean[]; + setPageLoadStates: React.Dispatch>; + pages: PageData[]; + setPages: React.Dispatch>; +} + +export interface ReaderProgressBarProps + extends Omit, + Pick {} + +export interface TReaderProgressCurrentPage extends PageData { + pagesIndex: number; +} + +export interface ReaderProgressBarSlotProps extends Pick { + pageName: string; + slotProps?: { + box?: BoxProps; + tooltip?: Omit; + }; +} + +export interface CurrentPageSlotProps extends Pick { + pageName: string; + currentPagesIndex: number; + pagesLength: number; + isDragging: boolean; + setIsDragging: (isDragging: boolean) => void; + boxProps?: BoxProps; +} diff --git a/src/modules/reader/utils/ReaderProgressBar.utils.tsx b/src/modules/reader/utils/ReaderProgressBar.utils.tsx new file mode 100644 index 00000000..8d05b2a7 --- /dev/null +++ b/src/modules/reader/utils/ReaderProgressBar.utils.tsx @@ -0,0 +1,139 @@ +/* + * 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 { 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'; + +export const getPage = (pageIndex: number, pages: ReaderProgressBarProps['pages']): TReaderProgressCurrentPage => { + const pagesIndex = pages.findIndex(({ primary, secondary }) => + [primary.index, secondary?.index].includes(pageIndex), + ); + const page = pages[pagesIndex]; + return { + ...page, + pagesIndex, + }; +}; + +export const getNextPageIndex = ( + offset: 'previous' | 'next', + pagesIndex: number, + pages: ReaderProgressBarProps['pages'], +): number => { + switch (offset) { + case 'previous': + return pages[Math.max(0, pagesIndex - 1)].primary.index; + case 'next': + return pages[Math.min(pages.length - 1, pagesIndex + 1)].primary.index; + default: + throw new Error(`Unexpected offset "${offset}"`); + } +}; + +export const getPageForMousePos = ( + coordinates: { clientX: number; clientY: number }, + elementRect: DOMRect, + pages: ReaderProgressBarProps['pages'], + isHorizontalPosition: boolean, + getOptionForDirection: typeof getOptionForDirectionImpl, +): ReaderProgressBarProps['pages'][number] => { + const pos = isHorizontalPosition ? coordinates.clientX : coordinates.clientY; + const rectPos = isHorizontalPosition ? elementRect.left : elementRect.top; + const rectSize = isHorizontalPosition ? elementRect.width : elementRect.height; + + const mouseXPosRelativeToProgressBar = pos - rectPos; + const pageForMouseXPos = Math.ceil((mouseXPosRelativeToProgressBar / rectSize) * pages.length); + const minPage = Math.max(1, pageForMouseXPos); + const maxPage = Math.min(minPage, pages.length); + const newPageIndex = getOptionForDirection(maxPage - 1, pages.length - maxPage); + + return pages[newPageIndex]; +}; + +export const getProgressBarPositionInfo = ( + position: ProgressBarPosition, +): { + isBottom: boolean; + isLeft: boolean; + isRight: boolean; + isHorizontal: boolean; + isVertical: boolean; +} => { + const isBottom = position === ProgressBarPosition.BOTTOM; + const isLeft = position === ProgressBarPosition.LEFT; + const isRight = position === ProgressBarPosition.RIGHT; + const isHorizontal = isBottom; + const isVertical = isLeft || isRight; + + return { + isBottom, + isLeft, + isRight, + isHorizontal, + isVertical, + }; +}; + +export const useHandleProgressDragging = ( + progressBarRef: RefObject, + isDragging: boolean, + currentPage: TReaderProgressCurrentPage, + setCurrentPageIndex: (pageIndex: number) => void, + pages: ReaderProgressBarProps['pages'], + progressBarPosition: ProgressBarPosition, + getOptionForDirection: typeof getOptionForDirectionImpl, +) => { + useEffect(() => { + if (!isDragging) { + return () => undefined; + } + + const { isHorizontal } = getProgressBarPositionInfo(progressBarPosition); + + const handleMove = (coordinates: { clientX: number; clientY: number }) => { + if (!progressBarRef.current) { + return; + } + + const newPageIndex = getPageForMousePos( + coordinates, + progressBarRef.current.getBoundingClientRect(), + pages, + isHorizontal, + getOptionForDirection, + ).primary.index; + + const hasCurrentPageIndexChanged = currentPage.primary.index !== newPageIndex; + if (!hasCurrentPageIndexChanged) { + return; + } + + setCurrentPageIndex(newPageIndex); + }; + + const handleMouseMove = (e: MouseEvent) => { + handleMove(e); + }; + + const handleTouchMove = (e: TouchEvent) => { + if (e.touches.length > 0) { + handleMove(e.touches[0]); + } + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('touchmove', handleTouchMove); + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('touchmove', handleTouchMove); + }; + }, [isDragging, currentPage, pages, progressBarPosition]); +}; diff --git a/src/modules/theme/ThemeDirectionCache.ts b/src/modules/theme/ThemeDirectionCache.ts new file mode 100644 index 00000000..9acf3aee --- /dev/null +++ b/src/modules/theme/ThemeDirectionCache.ts @@ -0,0 +1,23 @@ +/* + * 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 { Direction } from '@mui/material/styles'; +import { EmotionCache } from '@emotion/react'; +import createCache from '@emotion/cache'; +import { prefixer } from 'stylis'; +import rtlPlugin from 'stylis-plugin-rtl'; + +export const DIRECTION_TO_CACHE: Record = { + ltr: createCache({ + key: 'muiltr', + }), + rtl: createCache({ + key: 'muirtl', + stylisPlugins: [prefixer, rtlPlugin], + }), +}; diff --git a/src/theme.tsx b/src/theme.tsx index 10915c09..1e9f808f 100644 --- a/src/theme.tsx +++ b/src/theme.tsx @@ -148,8 +148,11 @@ export const createAndSetTheme = (...args: Parameters) => { return theme; }; -export const getOptionForDirection = (ltrOption: T, rtlOption: T): T => - (theme?.direction ?? 'ltr') === 'ltr' ? ltrOption : rtlOption; +export const getOptionForDirection = ( + ltrOption: T, + rtlOption: T, + direction: Theme['direction'] = theme?.direction ?? 'ltr', +): T => (direction === 'ltr' ? ltrOption : rtlOption); export const useGetOptionForDirection = (): typeof getOptionForDirection => { const muiTheme = useTheme();