diff --git a/src/base/components/SpinnerImage.tsx b/src/base/components/SpinnerImage.tsx index fd47f5c8..ffd6162d 100644 --- a/src/base/components/SpinnerImage.tsx +++ b/src/base/components/SpinnerImage.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useState, useEffect, forwardRef, ForwardedRef, useCallback, useRef } from 'react'; +import { useState, useEffect, useCallback, useRef, Ref } from 'react'; import CircularProgress from '@mui/material/CircularProgress'; import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; @@ -41,185 +41,185 @@ export interface SpinnerImageProps { priority?: Priority; retryKeyPrefix?: string; + + ref?: Ref; } -export const SpinnerImage = forwardRef( - (props: SpinnerImageProps, imgRef: ForwardedRef) => { - const { - shouldLoad = true, +export const SpinnerImage = ({ ref, ...props }: SpinnerImageProps) => { + const { + shouldLoad = true, + shouldDecode, + useFetchApi, + disableCors, + src, + alt, + onLoad, + onError, + spinnerStyle: { small, ...spinnerStyle } = {}, + imgStyle, + hideImgStyle, + priority, + retryKeyPrefix, + } = props; + + const { t } = useTranslation(); + + const loadingIndicatorRef = useRef(null); + + const showMissingImageIcon = !src.length; + + const [imageSourceUrl, setImageSourceUrl] = useState(); + const [imgLoadRetryKey, setImgLoadRetryKey] = useState(0); + const [isLoading, setIsLoading] = useState(); + const [hasError, setHasError] = useState(false); + const [isVisible, setIsVisible] = useState(false); + + const updateImageState = (loading: boolean, error: boolean = false, aborted: boolean = false) => { + setIsLoading(loading); + setHasError(error); + + if (error && !loading && !aborted) { + onError?.(); + } + + if (!loading && !error && !aborted) { + onLoad?.(); + } + }; + + useIntersectionObserver( + loadingIndicatorRef, + useCallback((entries) => setIsVisible(entries[0].isIntersecting), []), + ); + + useEffect(() => { + if (showMissingImageIcon || !shouldLoad) { + return () => {}; + } + + const imageRequest = requestManager.requestImage(src, { + priority, shouldDecode, useFetchApi, disableCors, - src, - alt, - onLoad, - onError, - spinnerStyle: { small, ...spinnerStyle } = {}, - imgStyle, - hideImgStyle, - priority, - retryKeyPrefix, - } = props; + }); + let cacheTimeout: NodeJS.Timeout; - const { t } = useTranslation(); + const fetchImage = async () => { + try { + const updateImage = async () => { + const image = await imageRequest.response; - const loadingIndicatorRef = useRef(null); + updateImageState(false); + setImageSourceUrl(image); + }; - const showMissingImageIcon = !src.length; + const checkCache = await Promise.race([ + imageRequest.response, + new Promise((resolve) => { + cacheTimeout = setTimeout(resolve, 50); + }), + ]); + const isImageCached = !!checkCache; - const [imageSourceUrl, setImageSourceUrl] = useState(); - const [imgLoadRetryKey, setImgLoadRetryKey] = useState(0); - const [isLoading, setIsLoading] = useState(); - const [hasError, setHasError] = useState(false); - const [isVisible, setIsVisible] = useState(false); + if (isImageCached) { + await updateImage(); + return; + } - const updateImageState = (loading: boolean, error: boolean = false, aborted: boolean = false) => { - setIsLoading(loading); - setHasError(error); - - if (error && !loading && !aborted) { - onError?.(); - } - - if (!loading && !error && !aborted) { - onLoad?.(); + updateImageState(true); + await updateImage(); + } catch (e) { + const wasAborted = + e instanceof Error && (e.name === 'AbortError' || e.message === 'Component was unmounted'); + updateImageState(false, !wasAborted, wasAborted); } }; - useIntersectionObserver( - loadingIndicatorRef, - useCallback((entries) => setIsVisible(entries[0].isIntersecting), []), - ); + fetchImage().catch(() => {}); - useEffect(() => { - if (showMissingImageIcon || !shouldLoad) { - return () => {}; - } + return () => { + imageRequest.cleanup(); + clearTimeout(cacheTimeout); + imageRequest.abortRequest(new Error('Component was unmounted')); + }; + }, [src, imgLoadRetryKey, retryKeyPrefix, showMissingImageIcon, shouldLoad]); - const imageRequest = requestManager.requestImage(src, { - priority, - shouldDecode, - useFetchApi, - disableCors, - }); - let cacheTimeout: NodeJS.Timeout; - - const fetchImage = async () => { - try { - const updateImage = async () => { - const image = await imageRequest.response; - - updateImageState(false); - setImageSourceUrl(image); - }; - - const checkCache = await Promise.race([ - imageRequest.response, - new Promise((resolve) => { - cacheTimeout = setTimeout(resolve, 50); - }), - ]); - const isImageCached = !!checkCache; - - if (isImageCached) { - await updateImage(); - return; - } - - updateImageState(true); - await updateImage(); - } catch (e) { - const wasAborted = - e instanceof Error && (e.name === 'AbortError' || e.message === 'Component was unmounted'); - updateImageState(false, !wasAborted, wasAborted); - } - }; - - fetchImage().catch(() => {}); - - return () => { - imageRequest.cleanup(); - clearTimeout(cacheTimeout); - imageRequest.abortRequest(new Error('Component was unmounted')); - }; - }, [src, imgLoadRetryKey, retryKeyPrefix, showMissingImageIcon, shouldLoad]); - - return ( - <> - {showMissingImageIcon ? ( - theme.palette.background.default, - ...spinnerStyle, - }} - > - - - ) : ( - + {showMissingImageIcon ? ( + theme.palette.background.default, + ...spinnerStyle, + }} + > + + + ) : ( + - )} + }), + ]} + ref={ref} + crossOrigin={disableCors ? undefined : 'anonymous'} + src={imageSourceUrl} + alt={alt} + draggable={false} + /> + )} - {(isLoading || (src && !imageSourceUrl) || hasError) && ( + {(isLoading || (src && !imageSourceUrl) || hasError) && ( + - - {isVisible && (isLoading || (src && !imageSourceUrl && !hasError)) && ( - - )} - {hasError && isLoading === false && ( - <> - - - - )} - + {isVisible && (isLoading || (src && !imageSourceUrl && !hasError)) && ( + + )} + {hasError && isLoading === false && ( + <> + + + + )} - )} - - ); - }, -); + + )} + + ); +}; diff --git a/src/base/components/buttons/CustomButton.tsx b/src/base/components/buttons/CustomButton.tsx index 2a3263c8..267f7ca6 100644 --- a/src/base/components/buttons/CustomButton.tsx +++ b/src/base/components/buttons/CustomButton.tsx @@ -8,25 +8,22 @@ import Button, { ButtonProps } from '@mui/material/Button'; import Stack from '@mui/material/Stack'; -import { ForwardedRef, forwardRef } from 'react'; -export const CustomButton = forwardRef( - ( - { children, ...props }: ButtonProps, - ref: ForwardedRef, - ) => ( - - ), +export const CustomButton = ({ + children, + ...props +}: ButtonProps) => ( + ); diff --git a/src/base/components/buttons/CustomButtonIcon.tsx b/src/base/components/buttons/CustomButtonIcon.tsx index 1ca07fd6..e9e00e00 100644 --- a/src/base/components/buttons/CustomButtonIcon.tsx +++ b/src/base/components/buttons/CustomButtonIcon.tsx @@ -7,23 +7,19 @@ */ import Button, { ButtonProps } from '@mui/material/Button'; -import { ForwardedRef, forwardRef } from 'react'; -export const CustomButtonIcon = forwardRef( - ( - { children, ...props }: ButtonProps, - ref: ForwardedRef, - ) => ( - - ), +export const CustomButtonIcon = ({ + children, + ...props +}: ButtonProps) => ( + ); diff --git a/src/base/components/feedback/SnackbarWithDescription.tsx b/src/base/components/feedback/SnackbarWithDescription.tsx index c6da28ba..baaf0aec 100644 --- a/src/base/components/feedback/SnackbarWithDescription.tsx +++ b/src/base/components/feedback/SnackbarWithDescription.tsx @@ -7,7 +7,7 @@ */ import { closeSnackbar, CustomContentProps, SnackbarContent, VariantType } from 'notistack'; -import { ForwardedRef, forwardRef, Fragment, memo } from 'react'; +import { ForwardedRef, Fragment, memo } from 'react'; import Alert from '@mui/material/Alert'; import AlertTitle from '@mui/material/AlertTitle'; import Button from '@mui/material/Button'; @@ -30,88 +30,84 @@ const SNACKBAR_VARIANT_TO_TRANSLATION_KEY: Record = }; export const SnackbarWithDescription = memo( - forwardRef( - ( - { - id, - message, - description, - variant, - action, - }: CustomContentProps & { - // eslint-disable-next-line react/no-unused-prop-types - description?: string; - }, - ref: ForwardedRef, - ) => { - const { t } = useTranslation(); - const theme = useTheme(); + ({ + id, + message, + description, + variant, + action, + ref, + }: CustomContentProps & { + description?: string; + ref?: ForwardedRef; + }) => { + const { t } = useTranslation(); + const theme = useTheme(); - const severity = variant === 'default' ? 'info' : variant; - const finalAction = typeof action === 'function' ? action(id) : action; + const severity = variant === 'default' ? 'info' : variant; + const finalAction = typeof action === 'function' ? action(id) : action; - const { isGraphqlException, graphqlError, graphqlStackTrace } = extractGraphqlExceptionInfo(description); + const { isGraphqlException, graphqlError, graphqlStackTrace } = extractGraphqlExceptionInfo(description); - const finalDescription = isGraphqlException ? graphqlError : description; - const isDescriptionTooLong = (finalDescription?.length ?? 0) > MAX_DESCRIPTION_LENGTH; - const actualDescription = isDescriptionTooLong - ? finalDescription?.slice(0, MAX_DESCRIPTION_LENGTH) - : finalDescription; + const finalDescription = isGraphqlException ? graphqlError : description; + const isDescriptionTooLong = (finalDescription?.length ?? 0) > MAX_DESCRIPTION_LENGTH; + const actualDescription = isDescriptionTooLong + ? finalDescription?.slice(0, MAX_DESCRIPTION_LENGTH) + : finalDescription; - const TitleComponent = actualDescription?.length ? AlertTitle : Fragment; + const TitleComponent = actualDescription?.length ? AlertTitle : Fragment; - return ( - - closeSnackbar(id)} - > - {message} - {actualDescription} - {isDescriptionTooLong || (isGraphqlException && graphqlStackTrace) ? ( - - ) : ( - '' - )} - - - ); - }, - ), + return ( + + closeSnackbar(id)} + > + {message} + {actualDescription} + {isDescriptionTooLong || (isGraphqlException && graphqlStackTrace) ? ( + + ) : ( + '' + )} + + + ); + }, ); diff --git a/src/base/components/menu/IconMenuItem.tsx b/src/base/components/menu/IconMenuItem.tsx index ad559ad7..45d6f2ab 100644 --- a/src/base/components/menu/IconMenuItem.tsx +++ b/src/base/components/menu/IconMenuItem.tsx @@ -13,7 +13,7 @@ import ListItemIcon from '@mui/material/ListItemIcon'; import MenuItem, { MenuItemProps as MuiMenuItemProps } from '@mui/material/MenuItem'; import { SxProps, Theme } from '@mui/material/styles'; -import React, { forwardRef, RefObject } from 'react'; +import React, { Ref } from 'react'; import { OverridableComponent } from '@mui/material/OverridableComponent'; import { SvgIconTypeMap } from '@mui/material/SvgIcon'; @@ -27,25 +27,31 @@ type IconMenuItemProps = { renderLabel?: () => React.ReactNode; LeftIcon?: OverridableComponent & { muiName: string }; onClick?: (event: React.MouseEvent) => void; - ref?: RefObject; + ref?: Ref; RightIcon?: OverridableComponent & { muiName: string }; sx?: SxProps; }; -export const IconMenuItem = forwardRef( - ({ MenuItemProps, className, label, LeftIcon, renderLabel, RightIcon, ...props }, ref) => ( - - {LeftIcon && ( - - - - )} - {label} - {RightIcon && ( - - - - )} - - ), +export const IconMenuItem = ({ + MenuItemProps, + className, + label, + LeftIcon, + renderLabel, + RightIcon, + ...props +}: IconMenuItemProps) => ( + + {LeftIcon && ( + + + + )} + {label} + {RightIcon && ( + + + + )} + ); diff --git a/src/base/components/menu/NestedMenuItem.tsx b/src/base/components/menu/NestedMenuItem.tsx index 03dc7b32..c998f7be 100644 --- a/src/base/components/menu/NestedMenuItem.tsx +++ b/src/base/components/menu/NestedMenuItem.tsx @@ -16,7 +16,6 @@ import Menu, { MenuProps as MuiMenuProps } from '@mui/material/Menu'; import { MenuItemProps as MuiMenuItemProps } from '@mui/material/MenuItem'; import { ElementType, - forwardRef, HTMLAttributes, KeyboardEvent, FocusEvent, @@ -25,6 +24,7 @@ import { RefAttributes, useRef, useState, + Ref, } from 'react'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; @@ -51,9 +51,10 @@ export type NestedMenuItemProps = Omit & { ContainerProps?: HTMLAttributes & RefAttributes; MenuProps?: Partial>; button?: true | undefined; + ref?: Ref; }; -const NestedMenuItem = forwardRef((props, ref) => { +export const NestedMenuItem = ({ ref, ...props }: NestedMenuItemProps) => { const { parentMenuOpen, label, @@ -229,7 +230,4 @@ const NestedMenuItem = forwardRef((pr ); -}); - -NestedMenuItem.displayName = 'NestedMenuItem'; -export { NestedMenuItem }; +}; diff --git a/src/base/components/tabs/TabsMenu.tsx b/src/base/components/tabs/TabsMenu.tsx index d201adc3..02cf2db4 100644 --- a/src/base/components/tabs/TabsMenu.tsx +++ b/src/base/components/tabs/TabsMenu.tsx @@ -8,7 +8,6 @@ import Tabs, { TabsProps } from '@mui/material/Tabs'; import { styled } from '@mui/material/styles'; -import { ForwardedRef, forwardRef } from 'react'; import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx'; const StyledTabsMenu = styled(Tabs)(({ theme }) => ({ @@ -24,23 +23,20 @@ const StyledTabsMenu = styled(Tabs)(({ theme }) => ({ borderColor: theme.palette.divider, })); -export const TabsMenu = forwardRef( - ({ children, sx, ...props }: TabsProps, ref: ForwardedRef) => { - const { appBarHeight } = useNavBarContext(); +export const TabsMenu = ({ children, sx, ...props }: TabsProps) => { + const { appBarHeight } = useNavBarContext(); - return ( - - {children} - - ); - }, -); + return ( + + {children} + + ); +}; diff --git a/src/base/hoc/withPropsFrom.tsx b/src/base/hoc/withPropsFrom.tsx index 76e9a78a..5a2b45fe 100644 --- a/src/base/hoc/withPropsFrom.tsx +++ b/src/base/hoc/withPropsFrom.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { ComponentType, memo, forwardRef } from 'react'; +import { ComponentType, memo, Ref } from 'react'; type PropsSourceCreator> = (props: Props) => T; @@ -21,19 +21,20 @@ export const withPropsFrom = < }, sourcePropKeys: SourcePropKeys[], ) => - memo( - forwardRef>((props, ref) => { - const sourceProps = propsSources.reduce( - (acc, propsSource) => ({ ...acc, ...propsSource(props as Omit) }), - {}, - ); + memo(({ ref, ...props }: Omit & { ref?: Ref }) => { + const sourceProps = propsSources.reduce( + (acc, propsSource) => ({ + ...acc, + ...propsSource(props as unknown as Omit), + }), + {}, + ); - const selectedProps = Object.fromEntries( - Object.entries(sourceProps).filter(([key]) => sourcePropKeys.includes(key as SourcePropKeys)), - ) as Pick, SourcePropKeys>; + const selectedProps = Object.fromEntries( + Object.entries(sourceProps).filter(([key]) => sourcePropKeys.includes(key as SourcePropKeys)), + ) as Pick, SourcePropKeys>; - const combinedProps = { ...props, ...selectedProps } as unknown as ComponentProps; + const combinedProps = { ...props, ...selectedProps } as unknown as ComponentProps; - return ; - }), - ); + return ; + }); diff --git a/src/features/manga/components/MangaGrid.tsx b/src/features/manga/components/MangaGrid.tsx index ef5debd6..7312c79f 100644 --- a/src/features/manga/components/MangaGrid.tsx +++ b/src/features/manga/components/MangaGrid.tsx @@ -6,16 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import React, { - ForwardedRef, - forwardRef, - useCallback, - useLayoutEffect, - useMemo, - useRef, - useState, - type JSX, -} from 'react'; +import React, { ForwardedRef, Ref, useCallback, useLayoutEffect, useMemo, useRef, useState, type JSX } from 'react'; import Grid, { GridTypeMap } from '@mui/material/Grid'; import Box, { BoxProps } from '@mui/material/Box'; import { GridItemProps } from 'react-virtuoso'; @@ -33,11 +24,11 @@ import { GridLayout } from '@/base/Base.types.ts'; import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts'; import { VirtuosoGridPersisted } from '@/lib/virtuoso/Component/VirtuosoGridPersisted.tsx'; -const GridContainer = React.forwardRef(({ children, ...props }, ref) => ( +const GridContainer = ({ children, ref, ...props }: GridTypeMap['props'] & { ref?: Ref }) => ( {children} -)); +); const GridItemContainerWithDimension = ( dimensions: number, @@ -86,115 +77,108 @@ type DefaultGridProps = Pick & { isSelectModeActive?: boolean; selectedMangaIds?: Required[]; handleSelection?: SelectableCollectionReturnType['handleSelection']; + ref?: ForwardedRef; }; -const HorizontalGrid = forwardRef( - ( - { - isLoading, - mangas, - inLibraryIndicator, - GridItemContainer, - gridLayout, - isSelectModeActive, - selectedMangaIds, - handleSelection, - mode, - }: DefaultGridProps, - ref: ForwardedRef, - ) => ( - - {isLoading ? ( - - ) : ( - mangas.map((manga) => ( - - {createMangaCard( - manga, - gridLayout, - inLibraryIndicator, - isSelectModeActive, - selectedMangaIds, - handleSelection, - mode, - )} - - )) - )} - - ), +const HorizontalGrid = ({ + isLoading, + mangas, + inLibraryIndicator, + GridItemContainer, + gridLayout, + isSelectModeActive, + selectedMangaIds, + handleSelection, + mode, + ref, +}: DefaultGridProps) => ( + + {isLoading ? ( + + ) : ( + mangas.map((manga) => ( + + {createMangaCard( + manga, + gridLayout, + inLibraryIndicator, + isSelectModeActive, + selectedMangaIds, + handleSelection, + mode, + )} + + )) + )} + ); export const MANGA_GRID_SNAPSHOT_KEY = 'MangaGrid-snapshot-location'; -const VerticalGrid = forwardRef( - ( - { - isLoading, - mangas, - inLibraryIndicator, - GridItemContainer, - gridLayout, - hasNextPage, - loadMore, - isSelectModeActive, - selectedMangaIds, - handleSelection, - mode, - }: DefaultGridProps & { - hasNextPage: boolean; - loadMore: () => void; - }, - ref: ForwardedRef, - ) => ( - <> - - loadMore()} - computeItemKey={(index) => mangas[index].id} - itemContent={(index) => - createMangaCard( - mangas[index], - gridLayout, - inLibraryIndicator, - isSelectModeActive, - selectedMangaIds, - handleSelection, - mode, - ) - } - /> - - {/* render div to prevent UI jumping around when showing/hiding loading placeholder */ - /* eslint-disable-next-line no-nested-ternary */} - {isSelectModeActive && gridLayout === GridLayout.List ? ( - - ) : // eslint-disable-next-line no-nested-ternary - isLoading ? ( - - ) : hasNextPage ? ( -
- ) : null} - - ), +const VerticalGrid = ({ + isLoading, + mangas, + inLibraryIndicator, + GridItemContainer, + gridLayout, + hasNextPage, + loadMore, + isSelectModeActive, + selectedMangaIds, + handleSelection, + mode, + ref, +}: DefaultGridProps & { + hasNextPage: boolean; + loadMore: () => void; +}) => ( + <> + + loadMore()} + computeItemKey={(index) => mangas[index].id} + itemContent={(index) => + createMangaCard( + mangas[index], + gridLayout, + inLibraryIndicator, + isSelectModeActive, + selectedMangaIds, + handleSelection, + mode, + ) + } + /> + + {/* render div to prevent UI jumping around when showing/hiding loading placeholder */ + /* eslint-disable-next-line no-nested-ternary */} + {isSelectModeActive && gridLayout === GridLayout.List ? ( + + ) : // eslint-disable-next-line no-nested-ternary + isLoading ? ( + + ) : hasNextPage ? ( +
+ ) : null} + ); export interface IMangaGridProps diff --git a/src/features/manga/components/MangaOptionButton.tsx b/src/features/manga/components/MangaOptionButton.tsx index f9b22490..29d43b51 100644 --- a/src/features/manga/components/MangaOptionButton.tsx +++ b/src/features/manga/components/MangaOptionButton.tsx @@ -7,7 +7,7 @@ */ import { useTranslation } from 'react-i18next'; -import { BaseSyntheticEvent, ChangeEvent, useMemo, forwardRef, ForwardedRef } from 'react'; +import { BaseSyntheticEvent, ChangeEvent, useMemo, ForwardedRef } from 'react'; import Button from '@mui/material/Button'; import Checkbox from '@mui/material/Checkbox'; import IconButton from '@mui/material/IconButton'; @@ -19,94 +19,91 @@ import { SelectableCollectionReturnType } from '@/features/collection/hooks/useS import { MangaType } from '@/lib/graphql/generated/graphql.ts'; import { MUIUtil } from '@/lib/mui/MUI.util.ts'; -export const MangaOptionButton = forwardRef( - ( - { - id, - selected, - handleSelection, - asCheckbox = false, - popupState, - }: { - id: number; - selected?: boolean | null; - handleSelection?: SelectableCollectionReturnType['handleSelection']; - asCheckbox?: boolean; - popupState: PopupState; - }, - ref: ForwardedRef, - ) => { - const { t } = useTranslation(); +export const MangaOptionButton = ({ + id, + selected, + handleSelection, + asCheckbox = false, + popupState, + ref, +}: { + id: number; + selected?: boolean | null; + handleSelection?: SelectableCollectionReturnType['handleSelection']; + asCheckbox?: boolean; + popupState: PopupState; + ref?: ForwardedRef; +}) => { + const { t } = useTranslation(); - const bindTriggerProps = useMemo(() => bindTrigger(popupState), [popupState]); + const bindTriggerProps = useMemo(() => bindTrigger(popupState), [popupState]); - const preventDefaultAction = (e: BaseSyntheticEvent) => { - e.stopPropagation(); - e.preventDefault(); - }; + const preventDefaultAction = (e: BaseSyntheticEvent) => { + e.stopPropagation(); + e.preventDefault(); + }; - const handleSelectionChange = (e: ChangeEvent, isSelected: boolean) => { - preventDefaultAction(e); - handleSelection?.(id, isSelected); - }; + const handleSelectionChange = (e: ChangeEvent, isSelected: boolean) => { + preventDefaultAction(e); + handleSelection?.(id, isSelected); + }; - if (!handleSelection) { + if (!handleSelection) { + return null; + } + + const isSelected = selected !== null; + if (isSelected) { + if (!asCheckbox) { return null; } - const isSelected = selected !== null; - if (isSelected) { - if (!asCheckbox) { - return null; - } - - return ( - - - - ); - } - - if (asCheckbox) { - return ( - - - - - - ); - } - return ( - - + + ); - }, -); + } + + if (asCheckbox) { + return ( + + + + + + ); + } + + return ( + + + + ); +}; diff --git a/src/features/reader/Reader.types.ts b/src/features/reader/Reader.types.ts index 08243380..8e579f05 100644 --- a/src/features/reader/Reader.types.ts +++ b/src/features/reader/Reader.types.ts @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { MutableRefObject } from 'react'; +import { MutableRefObject, Ref } from 'react'; import { TapZoneInvertMode, TapZoneLayouts } from '@/features/reader/tap-zones/TapZoneLayout.types.ts'; import { TChapterReader } from '@/features/chapter/Chapter.types.ts'; import { ReaderService } from '@/features/reader/services/ReaderService.ts'; @@ -349,6 +349,7 @@ export interface ReaderPagerProps isPreloadMode: boolean; resumeMode: ReaderResumeMode; handleAsInitialRender: boolean; + ref?: Ref; } export enum PageInViewportType { diff --git a/src/features/reader/overlay/mobile/ReaderOverlayHeaderMobile.tsx b/src/features/reader/overlay/mobile/ReaderOverlayHeaderMobile.tsx index 74e33b19..cb9d3026 100644 --- a/src/features/reader/overlay/mobile/ReaderOverlayHeaderMobile.tsx +++ b/src/features/reader/overlay/mobile/ReaderOverlayHeaderMobile.tsx @@ -17,7 +17,7 @@ import Link from '@mui/material/Link'; import { Link as RouterLink } from 'react-router-dom'; import { alpha } from '@mui/material/styles'; import Slide from '@mui/material/Slide'; -import { forwardRef, memo } from 'react'; +import { memo, Ref } from 'react'; import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx'; import { makeToast } from '@/base/utils/Toast.ts'; @@ -38,7 +38,7 @@ import { const DEFAULT_MANGA = { ...FALLBACK_MANGA, title: '' }; -const BaseReaderOverlayHeaderMobile = forwardRef(({ isVisible }, ref) => { +const BaseReaderOverlayHeaderMobile = ({ isVisible, ref }: MobileHeaderProps & { ref?: Ref }) => { const { t } = useTranslation(); const popupState = usePopupState({ popupId: 'reader-overlay-more-menu', variant: 'popover' }); const currentChapter = useReaderChaptersStore((state) => state.chapters.currentChapter); @@ -126,6 +126,6 @@ const BaseReaderOverlayHeaderMobile = forwardRef ); -}); +}; export const ReaderOverlayHeaderMobile = memo(BaseReaderOverlayHeaderMobile); diff --git a/src/features/reader/overlay/progress-bar/components/ReaderProgressBarDirectionWrapper.tsx b/src/features/reader/overlay/progress-bar/components/ReaderProgressBarDirectionWrapper.tsx index a07659e7..62cc2505 100644 --- a/src/features/reader/overlay/progress-bar/components/ReaderProgressBarDirectionWrapper.tsx +++ b/src/features/reader/overlay/progress-bar/components/ReaderProgressBarDirectionWrapper.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { forwardRef, useMemo } from 'react'; +import { useMemo } from 'react'; import { CacheProvider } from '@emotion/react'; import { ThemeProvider } from '@mui/material/styles'; import Box, { BoxProps } from '@mui/material/Box'; @@ -17,12 +17,12 @@ import { ReaderService } from '@/features/reader/services/ReaderService.ts'; import { DIRECTION_TO_CACHE } from '@/features/theme/ThemeDirectionCache.ts'; import { withPropsFrom } from '@/base/hoc/withPropsFrom.tsx'; -const BaseReaderProgressBarDirectionWrapper = forwardRef< - HTMLElement, - BoxProps & { - direction: ReturnType; - } ->(({ direction, ...boxProps }, ref) => { +const BaseReaderProgressBarDirectionWrapper = ({ + direction, + ...boxProps +}: BoxProps & { + direction: ReturnType; +}) => { const { settings: { customThemes, appTheme, themeMode, shouldUsePureBlackMode }, } = useMetadataServerSettings(); @@ -35,11 +35,11 @@ const BaseReaderProgressBarDirectionWrapper = forwardRef< return ( - + ); -}); +}; export const ReaderProgressBarDirectionWrapper = withPropsFrom( BaseReaderProgressBarDirectionWrapper, diff --git a/src/features/reader/viewer/ReaderViewer.tsx b/src/features/reader/viewer/ReaderViewer.tsx index b0173b4d..392819e9 100644 --- a/src/features/reader/viewer/ReaderViewer.tsx +++ b/src/features/reader/viewer/ReaderViewer.tsx @@ -6,17 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { - ForwardedRef, - forwardRef, - memo, - useCallback, - useEffect, - useLayoutEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { ForwardedRef, memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import Stack from '@mui/material/Stack'; import { useTheme } from '@mui/material/styles'; import { useLocation } from 'react-router-dom'; @@ -74,362 +64,356 @@ const READING_MODE_TO_IN_VIEWPORT_TYPE: Record [ReadingMode.WEBTOON]: PageInViewportType.Y, }; -const BaseReaderViewer = forwardRef( - ( - { - readerNavBarWidth, - updateCurrentPageIndex, - }: Pick & { - updateCurrentPageIndex: ReturnType; +const BaseReaderViewer = ({ + readerNavBarWidth, + updateCurrentPageIndex, + ref, +}: Pick & { + updateCurrentPageIndex: ReturnType; + ref?: ForwardedRef; +}) => { + const { direction: themeDirection } = useTheme(); + const isOverlayVisible = useReaderOverlayStore((state) => state.overlay.isVisible); + const { + currentPageIndex, + pageToScrollToIndex, + setPageToScrollToIndex, + pages, + totalPages, + setPages, + setPageLoadStates, + setTotalPages, + setCurrentPageIndex, + transitionPageMode, + retryFailedPagesKeyPrefix, + setTransitionPageMode, + } = useReaderPagesStore((state) => ({ + currentPageIndex: state.pages.currentPageIndex, + pageToScrollToIndex: state.pages.pageToScrollToIndex, + setPageToScrollToIndex: state.pages.setPageToScrollToIndex, + pages: state.pages.pages, + totalPages: state.pages.totalPages, + setPages: state.pages.setPages, + setPageLoadStates: state.pages.setPageLoadStates, + setTotalPages: state.pages.setTotalPages, + setCurrentPageIndex: state.pages.setCurrentPageIndex, + transitionPageMode: state.pages.transitionPageMode, + retryFailedPagesKeyPrefix: state.pages.retryFailedPagesKeyPrefix, + setTransitionPageMode: state.pages.setTransitionPageMode, + })); + const { initialChapter, currentChapter, chapters, visibleChapters, isCurrentChapterReady } = useReaderChaptersStore( + (state) => ({ + initialChapter: state.chapters.initialChapter, + currentChapter: state.chapters.currentChapter, + chapters: state.chapters.chapters, + visibleChapters: state.chapters.visibleChapters, + setReaderStateChapters: state.chapters.setReaderStateChapters, + isCurrentChapterReady: state.chapters.isCurrentChapterReady, + }), + ); + const { + readingMode, + readingDirection, + readerWidth, + pageScaleMode, + shouldOffsetDoubleSpreads, + imagePreLoadAmount, + pageGap, + customFilter, + shouldStretchPage, + isStaticNav, + } = useReaderSettingsStore((state) => ({ + readingMode: state.settings.readingMode.value, + readingDirection: state.settings.readingDirection.value, + readerWidth: state.settings.readerWidth.value, + pageScaleMode: state.settings.pageScaleMode.value, + shouldOffsetDoubleSpreads: state.settings.shouldOffsetDoubleSpreads.value, + imagePreLoadAmount: state.settings.imagePreLoadAmount, + pageGap: state.settings.pageGap.value, + customFilter: state.settings.customFilter, + shouldStretchPage: state.settings.shouldStretchPage.value, + isStaticNav: state.settings.isStaticNav, + })); + const { showPreview, setShowPreview } = useReaderTapZoneStore((state) => ({ + showPreview: state.tapZone.showPreview, + setShowPreview: state.tapZone.setShowPreview, + })); + const { resumeMode = ReaderResumeMode.START } = useLocation().state ?? { + resumeMode: ReaderResumeMode.START, + }; + + const scrollElementRef = useRef(null); + const mergedRef = useMergedRef(ref, scrollElementRef); + + const isContinuousVerticalReadingModeActive = isContinuousVerticalReadingMode(readingMode); + const isContinuousReadingModeActive = isContinuousReadingMode(readingMode); + const isDragging = useMouseDragScroll(scrollElementRef); + + const automaticScrolling = useReaderAutoScrollStore((state) => ({ + isPaused: state.autoScroll.isPaused, + pause: state.autoScroll.pause, + resume: state.autoScroll.resume, + setScrollRef: state.autoScroll.setScrollRef, + })); + useEffect(() => automaticScrolling.setScrollRef(scrollElementRef.current), []); + + const scrollbarXSize = MediaQuery.useGetScrollbarSize('width', scrollElementRef.current); + const scrollbarYSize = MediaQuery.useGetScrollbarSize('height', scrollElementRef.current); + useLayoutEffect(() => { + const { scrollbar } = getReaderStore(); + scrollbar.setXSize(scrollbarXSize); + scrollbar.setYSize(scrollbarYSize); + }, [scrollbarXSize, scrollbarYSize]); + + const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null)); + const [{ minChapterViewWidth, minChapterViewHeight, minChapterSizeSourceChapterId }, setChapterViewerSize] = + useState({ + minChapterViewWidth: 0, + minChapterViewHeight: 0, + minChapterSizeSourceChapterId: -1, + }); + + const [, setTriggerReRender] = useState({}); + + const inViewportType = READING_MODE_TO_IN_VIEWPORT_TYPE[readingMode]; + const isLtrReadingDirection = readingDirection === ReadingDirection.LTR; + const initialChapterIndex = useMemo( + () => chapters.findIndex((chapter) => chapter.id === initialChapter?.id), + [chapters, initialChapter?.id], + ); + const chaptersToRender = useMemo( + () => + chapters.slice( + Math.max(0, initialChapterIndex - visibleChapters.trailing), + Math.min(chapters.length, initialChapterIndex + visibleChapters.leading + 1), + ), + [chapters, initialChapterIndex, visibleChapters.trailing, visibleChapters.leading], + ); + const currentChapterIndex = useMemo( + () => chaptersToRender.findIndex((chapter) => chapter.id === currentChapter?.id), + [currentChapter, chaptersToRender], + ); + + const onChapterViewSizeChange = useCallback( + (width: number, height: number, chapterId: ChapterIdInfo['id']) => { + if (!isContinuousReadingModeActive) { + return; + } + + const isSameChapterId = chapterId === minChapterSizeSourceChapterId; + + if (isContinuousVerticalReadingModeActive) { + if (!isSameChapterId && minChapterViewWidth >= width) { + return; + } + + setChapterViewerSize({ + minChapterViewWidth: width, + minChapterViewHeight: 0, + minChapterSizeSourceChapterId: chapterId, + }); + return; + } + + if (isSameChapterId || minChapterViewHeight < height) { + setChapterViewerSize({ + minChapterViewWidth: 0, + minChapterViewHeight: height, + minChapterSizeSourceChapterId: chapterId, + }); + } }, + [ + isContinuousReadingModeActive, + isContinuousVerticalReadingModeActive, + minChapterViewWidth, + minChapterViewHeight, + minChapterSizeSourceChapterId, + ], + ); - ref: ForwardedRef, - ) => { - const { direction: themeDirection } = useTheme(); - const isOverlayVisible = useReaderOverlayStore((state) => state.overlay.isVisible); - const { - currentPageIndex, - pageToScrollToIndex, - setPageToScrollToIndex, - pages, - totalPages, - setPages, - setPageLoadStates, - setTotalPages, - setCurrentPageIndex, - transitionPageMode, - retryFailedPagesKeyPrefix, - setTransitionPageMode, - } = useReaderPagesStore((state) => ({ - currentPageIndex: state.pages.currentPageIndex, - pageToScrollToIndex: state.pages.pageToScrollToIndex, - setPageToScrollToIndex: state.pages.setPageToScrollToIndex, - pages: state.pages.pages, - totalPages: state.pages.totalPages, - setPages: state.pages.setPages, - setPageLoadStates: state.pages.setPageLoadStates, - setTotalPages: state.pages.setTotalPages, - setCurrentPageIndex: state.pages.setCurrentPageIndex, - transitionPageMode: state.pages.transitionPageMode, - retryFailedPagesKeyPrefix: state.pages.retryFailedPagesKeyPrefix, - setTransitionPageMode: state.pages.setTransitionPageMode, - })); - const { initialChapter, currentChapter, chapters, visibleChapters, isCurrentChapterReady } = - useReaderChaptersStore((state) => ({ - initialChapter: state.chapters.initialChapter, - currentChapter: state.chapters.currentChapter, - chapters: state.chapters.chapters, - visibleChapters: state.chapters.visibleChapters, - setReaderStateChapters: state.chapters.setReaderStateChapters, - isCurrentChapterReady: state.chapters.isCurrentChapterReady, - })); - const { - readingMode, - readingDirection, - readerWidth, - pageScaleMode, - shouldOffsetDoubleSpreads, - imagePreLoadAmount, - pageGap, - customFilter, - shouldStretchPage, - isStaticNav, - } = useReaderSettingsStore((state) => ({ - readingMode: state.settings.readingMode.value, - readingDirection: state.settings.readingDirection.value, - readerWidth: state.settings.readerWidth.value, - pageScaleMode: state.settings.pageScaleMode.value, - shouldOffsetDoubleSpreads: state.settings.shouldOffsetDoubleSpreads.value, - imagePreLoadAmount: state.settings.imagePreLoadAmount, - pageGap: state.settings.pageGap.value, - customFilter: state.settings.customFilter, - shouldStretchPage: state.settings.shouldStretchPage.value, - isStaticNav: state.settings.isStaticNav, - })); - const { showPreview, setShowPreview } = useReaderTapZoneStore((state) => ({ - showPreview: state.tapZone.showPreview, - setShowPreview: state.tapZone.setShowPreview, - })); - const { resumeMode = ReaderResumeMode.START } = useLocation().state ?? { - resumeMode: ReaderResumeMode.START, - }; + useReaderHandlePageSelection( + pageToScrollToIndex, + currentPageIndex, + pages, + totalPages, + setPageToScrollToIndex, + updateCurrentPageIndex, + isContinuousReadingModeActive, + imageRefs, + themeDirection, + readingDirection, + ); + useReaderScrollToStartOnPageChange( + currentPageIndex, + isContinuousReadingModeActive, + themeDirection, + readingDirection, + scrollElementRef, + ); + useReaderHideCursorOnInactivity(scrollElementRef); + useReaderHorizontalModeInvertXYScrolling(readingMode, readingDirection, scrollElementRef); + useReaderHideOverlayOnUserScroll(isOverlayVisible, showPreview, setShowPreview, scrollElementRef); + useReaderAutoScroll( + isOverlayVisible, + automaticScrolling.isPaused, + automaticScrolling.pause, + automaticScrolling.resume, + isStaticNav, + ); + useReaderPreserveScrollPosition( + scrollElementRef, + currentChapter?.id, + currentChapterIndex, + currentPageIndex, + chaptersToRender, + visibleChapters, + readingMode, + readingDirection, + setPageToScrollToIndex, + pageScaleMode, + ); - const scrollElementRef = useRef(null); - const mergedRef = useMergedRef(ref, scrollElementRef); + useLayoutEffect(() => { + setChapterViewerSize({ + minChapterViewWidth: 0, + minChapterViewHeight: 0, + minChapterSizeSourceChapterId: -1, + }); + setTriggerReRender({}); + }, [readingMode]); - const isContinuousVerticalReadingModeActive = isContinuousVerticalReadingMode(readingMode); - const isContinuousReadingModeActive = isContinuousReadingMode(readingMode); - const isDragging = useMouseDragScroll(scrollElementRef); + if (!initialChapter || !currentChapter) { + throw new Error('ReaderViewer: illegal state - initialChapter and currentChapter should not be undefined'); + } - const automaticScrolling = useReaderAutoScrollStore((state) => ({ - isPaused: state.autoScroll.isPaused, - pause: state.autoScroll.pause, - resume: state.autoScroll.resume, - setScrollRef: state.autoScroll.setScrollRef, - })); - useEffect(() => automaticScrolling.setScrollRef(scrollElementRef.current), []); - - const scrollbarXSize = MediaQuery.useGetScrollbarSize('width', scrollElementRef.current); - const scrollbarYSize = MediaQuery.useGetScrollbarSize('height', scrollElementRef.current); - useLayoutEffect(() => { - const { scrollbar } = getReaderStore(); - scrollbar.setXSize(scrollbarXSize); - scrollbar.setYSize(scrollbarYSize); - }, [scrollbarXSize, scrollbarYSize]); - - const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null)); - const [{ minChapterViewWidth, minChapterViewHeight, minChapterSizeSourceChapterId }, setChapterViewerSize] = - useState({ - minChapterViewWidth: 0, - minChapterViewHeight: 0, - minChapterSizeSourceChapterId: -1, - }); - - const [, setTriggerReRender] = useState({}); - - const inViewportType = READING_MODE_TO_IN_VIEWPORT_TYPE[readingMode]; - const isLtrReadingDirection = readingDirection === ReadingDirection.LTR; - const initialChapterIndex = useMemo( - () => chapters.findIndex((chapter) => chapter.id === initialChapter?.id), - [chapters, initialChapter?.id], - ); - const chaptersToRender = useMemo( - () => - chapters.slice( - Math.max(0, initialChapterIndex - visibleChapters.trailing), - Math.min(chapters.length, initialChapterIndex + visibleChapters.leading + 1), + return ( + chaptersToRender.findIndex((chapter) => chapter.id === currentChapter?.id), - [currentChapter, chaptersToRender], - ); - - const onChapterViewSizeChange = useCallback( - (width: number, height: number, chapterId: ChapterIdInfo['id']) => { - if (!isContinuousReadingModeActive) { - return; - } - - const isSameChapterId = chapterId === minChapterSizeSourceChapterId; - - if (isContinuousVerticalReadingModeActive) { - if (!isSameChapterId && minChapterViewWidth >= width) { - return; - } - - setChapterViewerSize({ - minChapterViewWidth: width, - minChapterViewHeight: 0, - minChapterSizeSourceChapterId: chapterId, - }); - return; - } - - if (isSameChapterId || minChapterViewHeight < height) { - setChapterViewerSize({ - minChapterViewWidth: 0, - minChapterViewHeight: height, - minChapterSizeSourceChapterId: chapterId, - }); - } - }, - [ - isContinuousReadingModeActive, - isContinuousVerticalReadingModeActive, - minChapterViewWidth, - minChapterViewHeight, - minChapterSizeSourceChapterId, - ], - ); - - useReaderHandlePageSelection( - pageToScrollToIndex, - currentPageIndex, - pages, - totalPages, - setPageToScrollToIndex, - updateCurrentPageIndex, - isContinuousReadingModeActive, - imageRefs, - themeDirection, - readingDirection, - ); - useReaderScrollToStartOnPageChange( - currentPageIndex, - isContinuousReadingModeActive, - themeDirection, - readingDirection, - scrollElementRef, - ); - useReaderHideCursorOnInactivity(scrollElementRef); - useReaderHorizontalModeInvertXYScrolling(readingMode, readingDirection, scrollElementRef); - useReaderHideOverlayOnUserScroll(isOverlayVisible, showPreview, setShowPreview, scrollElementRef); - useReaderAutoScroll( - isOverlayVisible, - automaticScrolling.isPaused, - automaticScrolling.pause, - automaticScrolling.resume, - isStaticNav, - ); - useReaderPreserveScrollPosition( - scrollElementRef, - currentChapter?.id, - currentChapterIndex, - currentPageIndex, - chaptersToRender, - visibleChapters, - readingMode, - readingDirection, - setPageToScrollToIndex, - pageScaleMode, - ); - - useLayoutEffect(() => { - setChapterViewerSize({ - minChapterViewWidth: 0, - minChapterViewHeight: 0, - minChapterSizeSourceChapterId: -1, - }); - setTriggerReRender({}); - }, [readingMode]); - - if (!initialChapter || !currentChapter) { - throw new Error('ReaderViewer: illegal state - initialChapter and currentChapter should not be undefined'); - } - - return ( - !isDragging && ReaderControls.handleClick(scrollElementRef.current, e)} - onScroll={() => - ReaderControls.updateCurrentPageOnScroll( - imageRefs, - totalPages - 1, - updateCurrentPageIndex, - inViewportType, - readingDirection, - ) - } - > - {chaptersToRender.map((_, index) => { - // chapters are sorted by latest to oldest, thus, loop over it in reversed order - const chapterIndex = Math.max(0, chaptersToRender.length - index - 1); - const chapter = chaptersToRender[chapterIndex]; + ...applyStyles(themeDirection === 'rtl', { + flexDirection: isLtrReadingDirection ? 'row-reverse' : 'row', + }), + }), + }} + onClick={(e) => !isDragging && ReaderControls.handleClick(scrollElementRef.current, e)} + onScroll={() => + ReaderControls.updateCurrentPageOnScroll( + imageRefs, + totalPages - 1, + updateCurrentPageIndex, + inViewportType, + readingDirection, + ) + } + > + {chaptersToRender.map((_, index) => { + // chapters are sorted by latest to oldest, thus, loop over it in reversed order + const chapterIndex = Math.max(0, chaptersToRender.length - index - 1); + const chapter = chaptersToRender[chapterIndex]; - const previousChapter = - chaptersToRender[chapterIndex + 1] ?? - chapters[initialChapterIndex + visibleChapters.leading + 1]; - const nextChapter = - chaptersToRender[chapterIndex - 1] ?? - chapters[initialChapterIndex - visibleChapters.trailing - 1]; + const previousChapter = + chaptersToRender[chapterIndex + 1] ?? chapters[initialChapterIndex + visibleChapters.leading + 1]; + const nextChapter = + chaptersToRender[chapterIndex - 1] ?? chapters[initialChapterIndex - visibleChapters.trailing - 1]; - const isInitialChapter = chapter.id === initialChapter.id; - const isCurrentChapter = chapter.id === currentChapter.id; - const isPreviousChapter = chapter.id === chaptersToRender[currentChapterIndex + 1]?.id; - const isNextChapter = chapter.id === chaptersToRender[currentChapterIndex - 1]?.id; - const isLeadingChapter = initialChapter.sourceOrder > chapter.sourceOrder; - const isTrailingChapter = initialChapter.sourceOrder < chapter.sourceOrder; - const isLastLeadingChapter = visibleChapters.lastLeadingChapterSourceOrder === chapter.sourceOrder; - const isLastTrailingChapter = - visibleChapters.lastTrailingChapterSourceOrder === chapter.sourceOrder; - const isPreloadMode = - (isLastLeadingChapter && visibleChapters.isLeadingChapterPreloadMode) || - (isLastTrailingChapter && visibleChapters.isTrailingChapterPreloadMode); + const isInitialChapter = chapter.id === initialChapter.id; + const isCurrentChapter = chapter.id === currentChapter.id; + const isPreviousChapter = chapter.id === chaptersToRender[currentChapterIndex + 1]?.id; + const isNextChapter = chapter.id === chaptersToRender[currentChapterIndex - 1]?.id; + const isLeadingChapter = initialChapter.sourceOrder > chapter.sourceOrder; + const isTrailingChapter = initialChapter.sourceOrder < chapter.sourceOrder; + const isLastLeadingChapter = visibleChapters.lastLeadingChapterSourceOrder === chapter.sourceOrder; + const isLastTrailingChapter = visibleChapters.lastTrailingChapterSourceOrder === chapter.sourceOrder; + const isPreloadMode = + (isLastLeadingChapter && visibleChapters.isLeadingChapterPreloadMode) || + (isLastTrailingChapter && visibleChapters.isTrailingChapterPreloadMode); - const previousNextChapterVisibility = getPreviousNextChapterVisibility( - chapterIndex, - chaptersToRender, - visibleChapters, - ); + const previousNextChapterVisibility = getPreviousNextChapterVisibility( + chapterIndex, + chaptersToRender, + visibleChapters, + ); - const isChapterSizeSourceChapter = chapter.id === minChapterSizeSourceChapterId; + const isChapterSizeSourceChapter = chapter.id === minChapterSizeSourceChapterId; - return ( - - ); - })} - - ); - }, -); + return ( + + ); + })} + + ); +}; export const ReaderViewer = withPropsFrom( memo(BaseReaderViewer), diff --git a/src/features/reader/viewer/pager/components/BasePager.tsx b/src/features/reader/viewer/pager/components/BasePager.tsx index fac86723..06a66d15 100644 --- a/src/features/reader/viewer/pager/components/BasePager.tsx +++ b/src/features/reader/viewer/pager/components/BasePager.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { forwardRef, memo, ReactNode, useCallback, useEffect, useMemo, useRef } from 'react'; +import { memo, ReactNode, useCallback, useEffect, useMemo, useRef } from 'react'; import Box, { BoxProps } from '@mui/material/Box'; import { getPageIndexesToLoad, isATransitionPageVisible } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx'; import { @@ -22,133 +22,117 @@ import { isContinuousReadingMode } from '@/features/reader/settings/ReaderSettin const getPreviousCurrentPageIndex = (resumeMode: ReaderResumeMode): number => resumeMode === ReaderResumeMode.END ? Number.MAX_SAFE_INTEGER : -1; -const BaseBasePager = forwardRef< - HTMLDivElement, - Omit & - Pick & { - createPage: ( - page: ReaderStatePages['pages'][number], - pagesIndex: number, - shouldLoad: boolean, - shouldDisplay: boolean, - setRef: (pagesIndex: number, element: HTMLElement | null) => void, - readingMode: ReaderPagerProps['readingMode'], - customFilter: ReaderPagerProps['customFilter'], - pageScaleMode: ReaderPagerProps['pageScaleMode'], - shouldStretchPage: ReaderPagerProps['shouldStretchPage'], - readerWidth: ReaderPagerProps['readerWidth'], - readerNavBarWidth: ReaderPagerProps['readerNavBarWidth'], - ) => ReactNode; - slots?: { boxProps?: BoxProps }; - } ->( - ( - { - currentPageIndex, - pages, - transitionPageMode, - imageRefs, - createPage, - slots, - readingMode, - imagePreLoadAmount, - isCurrentChapter, - isPreviousChapter, - isNextChapter, - customFilter, - pageScaleMode, - shouldStretchPage, - readerWidth, - readerNavBarWidth, - resumeMode, - handleAsInitialRender, - }, - ref, - ) => { - const previousCurrentPageIndex = useRef(getPreviousCurrentPageIndex(resumeMode)); +const BaseBasePager = ({ + currentPageIndex, + pages, + transitionPageMode, + imageRefs, + createPage, + slots, + readingMode, + imagePreLoadAmount, + isCurrentChapter, + isPreviousChapter, + isNextChapter, + customFilter, + pageScaleMode, + shouldStretchPage, + readerWidth, + readerNavBarWidth, + resumeMode, + handleAsInitialRender, + ref, +}: Omit & + Pick & { + createPage: ( + page: ReaderStatePages['pages'][number], + pagesIndex: number, + shouldLoad: boolean, + shouldDisplay: boolean, + setRef: (pagesIndex: number, element: HTMLElement | null) => void, + readingMode: ReaderPagerProps['readingMode'], + customFilter: ReaderPagerProps['customFilter'], + pageScaleMode: ReaderPagerProps['pageScaleMode'], + shouldStretchPage: ReaderPagerProps['shouldStretchPage'], + readerWidth: ReaderPagerProps['readerWidth'], + readerNavBarWidth: ReaderPagerProps['readerNavBarWidth'], + ) => ReactNode; + slots?: { boxProps?: BoxProps }; + }) => { + const previousCurrentPageIndex = useRef(getPreviousCurrentPageIndex(resumeMode)); - if (handleAsInitialRender) { - previousCurrentPageIndex.current = getPreviousCurrentPageIndex(resumeMode); - } + if (handleAsInitialRender) { + previousCurrentPageIndex.current = getPreviousCurrentPageIndex(resumeMode); + } - const pagesIndexesToRender = useMemo( - () => - getPageIndexesToLoad( - currentPageIndex, - pages, - previousCurrentPageIndex.current, - imagePreLoadAmount, - readingMode, - isCurrentChapter, - isPreviousChapter, - isNextChapter, - ), - [ + const pagesIndexesToRender = useMemo( + () => + getPageIndexesToLoad( currentPageIndex, pages, + previousCurrentPageIndex.current, imagePreLoadAmount, readingMode, isCurrentChapter, isPreviousChapter, isNextChapter, - ], - ); - useEffect(() => { - if (isCurrentChapter) { - previousCurrentPageIndex.current = currentPageIndex; - } - }, [pagesIndexesToRender, isCurrentChapter]); + ), + [currentPageIndex, pages, imagePreLoadAmount, readingMode, isCurrentChapter, isPreviousChapter, isNextChapter], + ); + useEffect(() => { + if (isCurrentChapter) { + previousCurrentPageIndex.current = currentPageIndex; + } + }, [pagesIndexesToRender, isCurrentChapter]); - const setRef = useCallback( - (pagesIndex: number, element: HTMLElement | null) => { - // eslint-disable-next-line no-param-reassign - imageRefs.current[pagesIndex] = element; - }, - [imageRefs], - ); + const setRef = useCallback( + (pagesIndex: number, element: HTMLElement | null) => { + // eslint-disable-next-line no-param-reassign + imageRefs.current[pagesIndex] = element; + }, + [imageRefs], + ); - return ( - - {pages.map((page, pagesIndex) => - createPage( - page, - pagesIndex, - pagesIndexesToRender.includes(pagesIndex), - [ReaderTransitionPageMode.NONE, ReaderTransitionPageMode.BOTH].includes(transitionPageMode), - setRef, - readingMode, - customFilter, - pageScaleMode, - shouldStretchPage, - readerWidth, - readerNavBarWidth, - ), - )} - - ); - }, -); + ), + ]} + > + {pages.map((page, pagesIndex) => + createPage( + page, + pagesIndex, + pagesIndexesToRender.includes(pagesIndex), + [ReaderTransitionPageMode.NONE, ReaderTransitionPageMode.BOTH].includes(transitionPageMode), + setRef, + readingMode, + customFilter, + pageScaleMode, + shouldStretchPage, + readerWidth, + readerNavBarWidth, + ), + )} + + ); +}; export const BasePager = memo(BaseBasePager); diff --git a/src/features/reader/viewer/pager/components/ReaderDoublePagedPager.tsx b/src/features/reader/viewer/pager/components/ReaderDoublePagedPager.tsx index 8e3467ee..97beb1dd 100644 --- a/src/features/reader/viewer/pager/components/ReaderDoublePagedPager.tsx +++ b/src/features/reader/viewer/pager/components/ReaderDoublePagedPager.tsx @@ -7,7 +7,7 @@ */ import { Direction, useTheme } from '@mui/material/styles'; -import { forwardRef, Fragment, memo, useMemo } from 'react'; +import { Fragment, memo, useMemo } from 'react'; import { BasePager } from '@/features/reader/viewer/pager/components/BasePager.tsx'; import { IReaderSettings, @@ -41,10 +41,14 @@ const getPagePosition = ( return isLtrReadingDirection ? 'right' : 'left'; }; -const BaseReaderDoublePagedPager = forwardRef< - HTMLDivElement, - ReaderPagerProps & Pick ->(({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => { +const BaseReaderDoublePagedPager = ({ + onLoad, + onError, + pageLoadStates, + retryFailedPagesKeyPrefix, + isPreloadMode, + ...props +}: ReaderPagerProps & Pick) => { const { currentPageIndex, pages, totalPages, readingDirection, pageScaleMode } = props; const { direction: themeDirection } = useTheme(); @@ -54,7 +58,6 @@ const BaseReaderDoublePagedPager = forwardRef< return ( { const { primary, secondary } = page; @@ -129,6 +132,6 @@ const BaseReaderDoublePagedPager = forwardRef< }} /> ); -}); +}; export const ReaderDoublePagedPager = memo(BaseReaderDoublePagedPager); diff --git a/src/features/reader/viewer/pager/components/ReaderHorizontalPager.tsx b/src/features/reader/viewer/pager/components/ReaderHorizontalPager.tsx index 99326654..7271f86c 100644 --- a/src/features/reader/viewer/pager/components/ReaderHorizontalPager.tsx +++ b/src/features/reader/viewer/pager/components/ReaderHorizontalPager.tsx @@ -7,16 +7,20 @@ */ import { useTheme } from '@mui/material/styles'; -import { forwardRef, memo } from 'react'; +import { memo } from 'react'; import { BasePager } from '@/features/reader/viewer/pager/components/BasePager.tsx'; import { applyStyles } from '@/base/utils/ApplyStyles.ts'; import { IReaderSettings, ReaderPagerProps, ReadingDirection } from '@/features/reader/Reader.types.ts'; import { createReaderPage } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx'; -const BaseReaderHorizontalPager = forwardRef< - HTMLDivElement, - ReaderPagerProps & Pick ->(({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => { +const BaseReaderHorizontalPager = ({ + onLoad, + onError, + pageLoadStates, + retryFailedPagesKeyPrefix, + isPreloadMode, + ...props +}: ReaderPagerProps & Pick) => { const { currentPageIndex, totalPages, pageGap, readingDirection } = props; const { direction: themeDirection } = useTheme(); @@ -25,7 +29,6 @@ const BaseReaderHorizontalPager = forwardRef< return ( createReaderPage( @@ -69,6 +72,6 @@ const BaseReaderHorizontalPager = forwardRef< }} /> ); -}); +}; export const ReaderHorizontalPager = memo(BaseReaderHorizontalPager); diff --git a/src/features/reader/viewer/pager/components/ReaderPagedPager.tsx b/src/features/reader/viewer/pager/components/ReaderPagedPager.tsx index ea585a01..20521f5e 100644 --- a/src/features/reader/viewer/pager/components/ReaderPagedPager.tsx +++ b/src/features/reader/viewer/pager/components/ReaderPagedPager.tsx @@ -6,46 +6,50 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { forwardRef, memo } from 'react'; +import { memo } from 'react'; import { BasePager } from '@/features/reader/viewer/pager/components/BasePager.tsx'; import { ReaderPagerProps } from '@/features/reader/Reader.types.ts'; import { createReaderPage } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx'; -const BaseReaderPagedPager = forwardRef( - ({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => { - const { currentPageIndex, totalPages } = props; +const BaseReaderPagedPager = ({ + onLoad, + onError, + pageLoadStates, + retryFailedPagesKeyPrefix, + isPreloadMode, + ...props +}: ReaderPagerProps) => { + const { currentPageIndex, totalPages } = props; - return ( - - createReaderPage( - page, - pagesIndex, - true, - pageLoadStates[page.primary.index].loaded, - isPreloadMode, - onLoad, - onError, - shouldLoad, - shouldDisplay && shouldLoad && currentPageIndex === page.primary.index, - currentPageIndex, - totalPages, - ...baseProps, - pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined, - ) - } - slots={{ - boxProps: { - sx: { - margin: 'auto', - }, + return ( + + createReaderPage( + page, + pagesIndex, + true, + pageLoadStates[page.primary.index].loaded, + isPreloadMode, + onLoad, + onError, + shouldLoad, + shouldDisplay && shouldLoad && currentPageIndex === page.primary.index, + currentPageIndex, + totalPages, + ...baseProps, + pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined, + ) + } + slots={{ + boxProps: { + sx: { + margin: 'auto', }, - }} - /> - ); - }, -); + }, + }} + /> + ); +}; export const ReaderPagedPager = memo(BaseReaderPagedPager); diff --git a/src/features/reader/viewer/pager/components/ReaderVerticalPager.tsx b/src/features/reader/viewer/pager/components/ReaderVerticalPager.tsx index a59112a2..ca96b189 100644 --- a/src/features/reader/viewer/pager/components/ReaderVerticalPager.tsx +++ b/src/features/reader/viewer/pager/components/ReaderVerticalPager.tsx @@ -6,47 +6,51 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { forwardRef, memo } from 'react'; +import { memo } from 'react'; import { BasePager } from '@/features/reader/viewer/pager/components/BasePager.tsx'; import { ReaderPagerProps, ReadingMode } from '@/features/reader/Reader.types.ts'; import { createReaderPage } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx'; -const BaseReaderVerticalPager = forwardRef( - ({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => { - const { currentPageIndex, totalPages, readingMode, pageGap } = props; +const BaseReaderVerticalPager = ({ + onLoad, + onError, + pageLoadStates, + retryFailedPagesKeyPrefix, + isPreloadMode, + ...props +}: ReaderPagerProps) => { + const { currentPageIndex, totalPages, readingMode, pageGap } = props; - const isWebtoonMode = readingMode === ReadingMode.WEBTOON; - const actualPageGap = isWebtoonMode ? 0 : pageGap; + const isWebtoonMode = readingMode === ReadingMode.WEBTOON; + const actualPageGap = isWebtoonMode ? 0 : pageGap; - return ( - - createReaderPage( - page, - pagesIndex, - true, - pageLoadStates[page.primary.index].loaded, - isPreloadMode, - onLoad, - onError, - shouldLoad, - !isPreloadMode, - currentPageIndex, - totalPages, - ...baseProps, - pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined, - undefined, - undefined, - page.primary.index !== 0 ? actualPageGap : 0, - setRef, - ) - } - slots={{ boxProps: { sx: { margin: 'auto' } } }} - /> - ); - }, -); + return ( + + createReaderPage( + page, + pagesIndex, + true, + pageLoadStates[page.primary.index].loaded, + isPreloadMode, + onLoad, + onError, + shouldLoad, + !isPreloadMode, + currentPageIndex, + totalPages, + ...baseProps, + pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined, + undefined, + undefined, + page.primary.index !== 0 ? actualPageGap : 0, + setRef, + ) + } + slots={{ boxProps: { sx: { margin: 'auto' } } }} + /> + ); +}; export const ReaderVerticalPager = memo(BaseReaderVerticalPager);