Remove "forwardRef" usage

This commit is contained in:
schroda
2025-09-24 00:51:13 +02:00
parent bcb5e391f0
commit a2d7091712
19 changed files with 1055 additions and 1101 deletions

View File

@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
@@ -41,185 +41,185 @@ export interface SpinnerImageProps {
priority?: Priority; priority?: Priority;
retryKeyPrefix?: string; retryKeyPrefix?: string;
ref?: Ref<HTMLImageElement | HTMLDivElement | null>;
} }
export const SpinnerImage = forwardRef( export const SpinnerImage = ({ ref, ...props }: SpinnerImageProps) => {
(props: SpinnerImageProps, imgRef: ForwardedRef<HTMLImageElement | HTMLDivElement | null>) => { const {
const { shouldLoad = true,
shouldLoad = true, shouldDecode,
useFetchApi,
disableCors,
src,
alt,
onLoad,
onError,
spinnerStyle: { small, ...spinnerStyle } = {},
imgStyle,
hideImgStyle,
priority,
retryKeyPrefix,
} = props;
const { t } = useTranslation();
const loadingIndicatorRef = useRef<HTMLDivElement | null>(null);
const showMissingImageIcon = !src.length;
const [imageSourceUrl, setImageSourceUrl] = useState<string>();
const [imgLoadRetryKey, setImgLoadRetryKey] = useState(0);
const [isLoading, setIsLoading] = useState<boolean>();
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, shouldDecode,
useFetchApi, useFetchApi,
disableCors, disableCors,
src, });
alt, let cacheTimeout: NodeJS.Timeout;
onLoad,
onError,
spinnerStyle: { small, ...spinnerStyle } = {},
imgStyle,
hideImgStyle,
priority,
retryKeyPrefix,
} = props;
const { t } = useTranslation(); const fetchImage = async () => {
try {
const updateImage = async () => {
const image = await imageRequest.response;
const loadingIndicatorRef = useRef<HTMLDivElement | null>(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<string>(); if (isImageCached) {
const [imgLoadRetryKey, setImgLoadRetryKey] = useState(0); await updateImage();
const [isLoading, setIsLoading] = useState<boolean>(); return;
const [hasError, setHasError] = useState(false); }
const [isVisible, setIsVisible] = useState(false);
const updateImageState = (loading: boolean, error: boolean = false, aborted: boolean = false) => { updateImageState(true);
setIsLoading(loading); await updateImage();
setHasError(error); } catch (e) {
const wasAborted =
if (error && !loading && !aborted) { e instanceof Error && (e.name === 'AbortError' || e.message === 'Component was unmounted');
onError?.(); updateImageState(false, !wasAborted, wasAborted);
}
if (!loading && !error && !aborted) {
onLoad?.();
} }
}; };
useIntersectionObserver( fetchImage().catch(() => {});
loadingIndicatorRef,
useCallback((entries) => setIsVisible(entries[0].isIntersecting), []),
);
useEffect(() => { return () => {
if (showMissingImageIcon || !shouldLoad) { imageRequest.cleanup();
return () => {}; clearTimeout(cacheTimeout);
} imageRequest.abortRequest(new Error('Component was unmounted'));
};
}, [src, imgLoadRetryKey, retryKeyPrefix, showMissingImageIcon, shouldLoad]);
const imageRequest = requestManager.requestImage(src, { return (
priority, <>
shouldDecode, {showMissingImageIcon ? (
useFetchApi, <Stack
disableCors, ref={ref}
}); sx={{
let cacheTimeout: NodeJS.Timeout; height: '100%',
alignItems: 'center',
const fetchImage = async () => { justifyContent: 'center',
try { background: (theme) => theme.palette.background.default,
const updateImage = async () => { ...spinnerStyle,
const image = await imageRequest.response; }}
>
updateImageState(false); <ImageIcon fontSize="large" />
setImageSourceUrl(image); </Stack>
}; ) : (
<Box
const checkCache = await Promise.race([ component="img"
imageRequest.response, key={`${src}_${imgLoadRetryKey}_${retryKeyPrefix}`}
new Promise((resolve) => { sx={[
cacheTimeout = setTimeout(resolve, 50); ...(Array.isArray(imgStyle) ? (imgStyle ?? []) : [imgStyle]),
}), applyStyles(!imageSourceUrl || isLoading || hasError, {
]); ...hideImgStyle,
const isImageCached = !!checkCache; ...applyStyles(!hideImgStyle, {
display: 'none',
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 ? (
<Stack
ref={imgRef}
sx={{
height: '100%',
alignItems: 'center',
justifyContent: 'center',
background: (theme) => theme.palette.background.default,
...spinnerStyle,
}}
>
<ImageIcon fontSize="large" />
</Stack>
) : (
<Box
component="img"
key={`${src}_${imgLoadRetryKey}_${retryKeyPrefix}`}
sx={[
...(Array.isArray(imgStyle) ? (imgStyle ?? []) : [imgStyle]),
applyStyles(!imageSourceUrl || isLoading || hasError, {
...hideImgStyle,
...applyStyles(!hideImgStyle, {
display: 'none',
}),
}), }),
]} }),
ref={imgRef} ]}
crossOrigin={disableCors ? undefined : 'anonymous'} ref={ref}
src={imageSourceUrl} crossOrigin={disableCors ? undefined : 'anonymous'}
alt={alt} src={imageSourceUrl}
draggable={false} alt={alt}
/> draggable={false}
)} />
)}
{(isLoading || (src && !imageSourceUrl) || hasError) && ( {(isLoading || (src && !imageSourceUrl) || hasError) && (
<Stack
ref={loadingIndicatorRef}
sx={{
height: '100%',
justifyContent: 'center',
alignItems: 'center',
...spinnerStyle,
}}
>
<Stack <Stack
ref={loadingIndicatorRef}
sx={{ sx={{
height: '100%', height: '100%',
justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
...spinnerStyle, justifyContent: 'center',
}} }}
> >
<Stack {isVisible && (isLoading || (src && !imageSourceUrl && !hasError)) && (
sx={{ <CircularProgress thickness={5} />
height: '100%', )}
alignItems: 'center', {hasError && isLoading === false && (
justifyContent: 'center', <>
}} <BrokenImageIcon />
> <Button
{isVisible && (isLoading || (src && !imageSourceUrl && !hasError)) && ( startIcon={!small && <RefreshIcon />}
<CircularProgress thickness={5} /> onClick={(e) => {
)} e.stopPropagation();
{hasError && isLoading === false && ( e.preventDefault();
<> setImgLoadRetryKey((prevState) => (prevState + 1) % 100);
<BrokenImageIcon /> }}
<Button size={small ? 'small' : 'large'}
startIcon={!small && <RefreshIcon />} >
onClick={(e) => { {small ? <RefreshIcon /> : t('global.button.retry')}
e.stopPropagation(); </Button>
e.preventDefault(); </>
setImgLoadRetryKey((prevState) => (prevState + 1) % 100); )}
}}
size={small ? 'small' : 'large'}
>
{small ? <RefreshIcon /> : t('global.button.retry')}
</Button>
</>
)}
</Stack>
</Stack> </Stack>
)} </Stack>
</> )}
); </>
}, );
); };

View File

@@ -8,25 +8,22 @@
import Button, { ButtonProps } from '@mui/material/Button'; import Button, { ButtonProps } from '@mui/material/Button';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import { ForwardedRef, forwardRef } from 'react';
export const CustomButton = forwardRef( export const CustomButton = <C extends React.ElementType>({
<C extends React.ElementType>( children,
{ children, ...props }: ButtonProps<C, { component?: C }>, ...props
ref: ForwardedRef<HTMLButtonElement | null>, }: ButtonProps<C, { component?: C }>) => (
) => ( <Button {...props}>
<Button ref={ref} {...props}> <Stack
<Stack direction="row"
direction="row" sx={{
sx={{ alignItems: 'center',
alignItems: 'center', justifyContent: 'center',
justifyContent: 'center', gap: 1,
gap: 1, flexWrap: 'wrap',
flexWrap: 'wrap', }}
}} >
> {children}
{children} </Stack>
</Stack> </Button>
</Button>
),
); );

View File

@@ -7,23 +7,19 @@
*/ */
import Button, { ButtonProps } from '@mui/material/Button'; import Button, { ButtonProps } from '@mui/material/Button';
import { ForwardedRef, forwardRef } from 'react';
export const CustomButtonIcon = forwardRef( export const CustomButtonIcon = <C extends React.ElementType>({
<C extends React.ElementType>( children,
{ children, ...props }: ButtonProps<C, { component?: C }>, ...props
ref: ForwardedRef<HTMLButtonElement | null>, }: ButtonProps<C, { component?: C }>) => (
) => ( <Button
<Button {...props}
ref={ref} sx={{
{...props} minWidth: 'unset',
sx={{ px: '10px',
minWidth: 'unset', ...props.sx,
px: '10px', }}
...props.sx, >
}} {children}
> </Button>
{children}
</Button>
),
); );

View File

@@ -7,7 +7,7 @@
*/ */
import { closeSnackbar, CustomContentProps, SnackbarContent, VariantType } from 'notistack'; 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 Alert from '@mui/material/Alert';
import AlertTitle from '@mui/material/AlertTitle'; import AlertTitle from '@mui/material/AlertTitle';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
@@ -30,88 +30,84 @@ const SNACKBAR_VARIANT_TO_TRANSLATION_KEY: Record<VariantType, TranslationKey> =
}; };
export const SnackbarWithDescription = memo( export const SnackbarWithDescription = memo(
forwardRef( ({
( id,
{ message,
id, description,
message, variant,
description, action,
variant, ref,
action, }: CustomContentProps & {
}: CustomContentProps & { description?: string;
// eslint-disable-next-line react/no-unused-prop-types ref?: ForwardedRef<HTMLDivElement>;
description?: string; }) => {
}, const { t } = useTranslation();
ref: ForwardedRef<HTMLDivElement>, const theme = useTheme();
) => {
const { t } = useTranslation();
const theme = useTheme();
const severity = variant === 'default' ? 'info' : variant; const severity = variant === 'default' ? 'info' : variant;
const finalAction = typeof action === 'function' ? action(id) : action; 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 finalDescription = isGraphqlException ? graphqlError : description;
const isDescriptionTooLong = (finalDescription?.length ?? 0) > MAX_DESCRIPTION_LENGTH; const isDescriptionTooLong = (finalDescription?.length ?? 0) > MAX_DESCRIPTION_LENGTH;
const actualDescription = isDescriptionTooLong const actualDescription = isDescriptionTooLong
? finalDescription?.slice(0, MAX_DESCRIPTION_LENGTH) ? finalDescription?.slice(0, MAX_DESCRIPTION_LENGTH)
: finalDescription; : finalDescription;
const TitleComponent = actualDescription?.length ? AlertTitle : Fragment; const TitleComponent = actualDescription?.length ? AlertTitle : Fragment;
return ( return (
<SnackbarContent ref={ref}> <SnackbarContent ref={ref}>
<Alert <Alert
elevation={1} elevation={1}
severity={severity} severity={severity}
action={finalAction} action={finalAction}
sx={{ sx={{
wordBreak: 'break-word', wordBreak: 'break-word',
minWidth: '300px', minWidth: '300px',
[theme.breakpoints.down(MediaQuery.MOBILE_WIDTH)]: { [theme.breakpoints.down(MediaQuery.MOBILE_WIDTH)]: {
maxWidth: '100vw', maxWidth: '100vw',
}, },
[theme.breakpoints.between(MediaQuery.MOBILE_WIDTH, MediaQuery.TABLET_WIDTH)]: { [theme.breakpoints.between(MediaQuery.MOBILE_WIDTH, MediaQuery.TABLET_WIDTH)]: {
maxWidth: '75vw', maxWidth: '75vw',
}, },
[theme.breakpoints.up(MediaQuery.TABLET_WIDTH)]: { [theme.breakpoints.up(MediaQuery.TABLET_WIDTH)]: {
maxWidth: '50vw', maxWidth: '50vw',
}, },
}} }}
onClose={() => closeSnackbar(id)} onClose={() => closeSnackbar(id)}
> >
<TitleComponent>{message}</TitleComponent> <TitleComponent>{message}</TitleComponent>
{actualDescription} {actualDescription}
{isDescriptionTooLong || (isGraphqlException && graphqlStackTrace) ? ( {isDescriptionTooLong || (isGraphqlException && graphqlStackTrace) ? (
<Button <Button
onClick={() => { onClick={() => {
awaitConfirmation({ awaitConfirmation({
title: title:
typeof message === 'string' typeof message === 'string'
? message ? message
: t(SNACKBAR_VARIANT_TO_TRANSLATION_KEY[variant]), : t(SNACKBAR_VARIANT_TO_TRANSLATION_KEY[variant]),
message: description ?? '', message: description ?? '',
actions: { actions: {
cancel: { show: false }, cancel: { show: false },
confirm: { title: t('global.label.close') }, confirm: { title: t('global.label.close') },
}, },
}).catch( }).catch(
defaultPromiseErrorHandler( defaultPromiseErrorHandler(
`SnackbarWithDescription: ${id} - ${message} - ${description}`, `SnackbarWithDescription: ${id} - ${message} - ${description}`,
), ),
); );
}} }}
size="small" size="small"
> >
{t('global.button.show_more')} {t('global.button.show_more')}
</Button> </Button>
) : ( ) : (
'' ''
)} )}
</Alert> </Alert>
</SnackbarContent> </SnackbarContent>
); );
}, },
),
); );

View File

@@ -13,7 +13,7 @@
import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemIcon from '@mui/material/ListItemIcon';
import MenuItem, { MenuItemProps as MuiMenuItemProps } from '@mui/material/MenuItem'; import MenuItem, { MenuItemProps as MuiMenuItemProps } from '@mui/material/MenuItem';
import { SxProps, Theme } from '@mui/material/styles'; 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 { OverridableComponent } from '@mui/material/OverridableComponent';
import { SvgIconTypeMap } from '@mui/material/SvgIcon'; import { SvgIconTypeMap } from '@mui/material/SvgIcon';
@@ -27,25 +27,31 @@ type IconMenuItemProps = {
renderLabel?: () => React.ReactNode; renderLabel?: () => React.ReactNode;
LeftIcon?: OverridableComponent<SvgIconTypeMap> & { muiName: string }; LeftIcon?: OverridableComponent<SvgIconTypeMap> & { muiName: string };
onClick?: (event: React.MouseEvent<HTMLElement>) => void; onClick?: (event: React.MouseEvent<HTMLElement>) => void;
ref?: RefObject<HTMLLIElement | null>; ref?: Ref<HTMLLIElement | null>;
RightIcon?: OverridableComponent<SvgIconTypeMap> & { muiName: string }; RightIcon?: OverridableComponent<SvgIconTypeMap> & { muiName: string };
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
}; };
export const IconMenuItem = forwardRef<HTMLLIElement, IconMenuItemProps>( export const IconMenuItem = ({
({ MenuItemProps, className, label, LeftIcon, renderLabel, RightIcon, ...props }, ref) => ( MenuItemProps,
<MenuItem {...MenuItemProps} ref={ref} className={className} {...props}> className,
{LeftIcon && ( label,
<ListItemIcon> LeftIcon,
<LeftIcon fontSize="small" /> renderLabel,
</ListItemIcon> RightIcon,
)} ...props
<ListItemText>{label}</ListItemText> }: IconMenuItemProps) => (
{RightIcon && ( <MenuItem {...MenuItemProps} className={className} {...props}>
<ListItemIcon style={{ minWidth: 0 }}> {LeftIcon && (
<RightIcon fontSize="small" /> <ListItemIcon>
</ListItemIcon> <LeftIcon fontSize="small" />
)} </ListItemIcon>
</MenuItem> )}
), <ListItemText>{label}</ListItemText>
{RightIcon && (
<ListItemIcon style={{ minWidth: 0 }}>
<RightIcon fontSize="small" />
</ListItemIcon>
)}
</MenuItem>
); );

View File

@@ -16,7 +16,6 @@ import Menu, { MenuProps as MuiMenuProps } from '@mui/material/Menu';
import { MenuItemProps as MuiMenuItemProps } from '@mui/material/MenuItem'; import { MenuItemProps as MuiMenuItemProps } from '@mui/material/MenuItem';
import { import {
ElementType, ElementType,
forwardRef,
HTMLAttributes, HTMLAttributes,
KeyboardEvent, KeyboardEvent,
FocusEvent, FocusEvent,
@@ -25,6 +24,7 @@ import {
RefAttributes, RefAttributes,
useRef, useRef,
useState, useState,
Ref,
} from 'react'; } from 'react';
import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
@@ -51,9 +51,10 @@ export type NestedMenuItemProps = Omit<MuiMenuItemProps, 'button'> & {
ContainerProps?: HTMLAttributes<HTMLElement> & RefAttributes<HTMLElement>; ContainerProps?: HTMLAttributes<HTMLElement> & RefAttributes<HTMLElement>;
MenuProps?: Partial<Omit<MuiMenuProps, 'children'>>; MenuProps?: Partial<Omit<MuiMenuProps, 'children'>>;
button?: true | undefined; button?: true | undefined;
ref?: Ref<HTMLLIElement | null>;
}; };
const NestedMenuItem = forwardRef<HTMLLIElement | null, NestedMenuItemProps>((props, ref) => { export const NestedMenuItem = ({ ref, ...props }: NestedMenuItemProps) => {
const { const {
parentMenuOpen, parentMenuOpen,
label, label,
@@ -229,7 +230,4 @@ const NestedMenuItem = forwardRef<HTMLLIElement | null, NestedMenuItemProps>((pr
</Menu> </Menu>
</Box> </Box>
); );
}); };
NestedMenuItem.displayName = 'NestedMenuItem';
export { NestedMenuItem };

View File

@@ -8,7 +8,6 @@
import Tabs, { TabsProps } from '@mui/material/Tabs'; import Tabs, { TabsProps } from '@mui/material/Tabs';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import { ForwardedRef, forwardRef } from 'react';
import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx'; import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx';
const StyledTabsMenu = styled(Tabs)(({ theme }) => ({ const StyledTabsMenu = styled(Tabs)(({ theme }) => ({
@@ -24,23 +23,20 @@ const StyledTabsMenu = styled(Tabs)(({ theme }) => ({
borderColor: theme.palette.divider, borderColor: theme.palette.divider,
})); }));
export const TabsMenu = forwardRef( export const TabsMenu = ({ children, sx, ...props }: TabsProps) => {
({ children, sx, ...props }: TabsProps, ref: ForwardedRef<HTMLDivElement | null>) => { const { appBarHeight } = useNavBarContext();
const { appBarHeight } = useNavBarContext();
return ( return (
<StyledTabsMenu <StyledTabsMenu
sx={{ ...sx, top: appBarHeight }} sx={{ ...sx, top: appBarHeight }}
ref={ref} indicatorColor="primary"
indicatorColor="primary" textColor="primary"
textColor="primary" variant="scrollable"
variant="scrollable" scrollButtons="auto"
scrollButtons="auto" allowScrollButtonsMobile
allowScrollButtonsMobile {...props}
{...props} >
> {children}
{children} </StyledTabsMenu>
</StyledTabsMenu> );
); };
},
);

View File

@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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<T, Props extends Record<string, any>> = (props: Props) => T; type PropsSourceCreator<T, Props extends Record<string, any>> = (props: Props) => T;
@@ -21,19 +21,20 @@ export const withPropsFrom = <
}, },
sourcePropKeys: SourcePropKeys[], sourcePropKeys: SourcePropKeys[],
) => ) =>
memo( memo(({ ref, ...props }: Omit<ComponentProps, SourcePropKeys> & { ref?: Ref<HTMLElement> }) => {
forwardRef<HTMLElement, Omit<ComponentProps, SourcePropKeys>>((props, ref) => { const sourceProps = propsSources.reduce(
const sourceProps = propsSources.reduce( (acc, propsSource) => ({
(acc, propsSource) => ({ ...acc, ...propsSource(props as Omit<ComponentProps, SourcePropKeys>) }), ...acc,
{}, ...propsSource(props as unknown as Omit<ComponentProps, SourcePropKeys>),
); }),
{},
);
const selectedProps = Object.fromEntries( const selectedProps = Object.fromEntries(
Object.entries(sourceProps).filter(([key]) => sourcePropKeys.includes(key as SourcePropKeys)), Object.entries(sourceProps).filter(([key]) => sourcePropKeys.includes(key as SourcePropKeys)),
) as Pick<MergeObjectsArray<SourceProps>, SourcePropKeys>; ) as Pick<MergeObjectsArray<SourceProps>, SourcePropKeys>;
const combinedProps = { ...props, ...selectedProps } as unknown as ComponentProps; const combinedProps = { ...props, ...selectedProps } as unknown as ComponentProps;
return <Component {...combinedProps} ref={ref} />; return <Component {...combinedProps} ref={ref} />;
}), });
);

View File

@@ -6,16 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import React, { import React, { ForwardedRef, Ref, useCallback, useLayoutEffect, useMemo, useRef, useState, type JSX } from 'react';
ForwardedRef,
forwardRef,
useCallback,
useLayoutEffect,
useMemo,
useRef,
useState,
type JSX,
} from 'react';
import Grid, { GridTypeMap } from '@mui/material/Grid'; import Grid, { GridTypeMap } from '@mui/material/Grid';
import Box, { BoxProps } from '@mui/material/Box'; import Box, { BoxProps } from '@mui/material/Box';
import { GridItemProps } from 'react-virtuoso'; 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 { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
import { VirtuosoGridPersisted } from '@/lib/virtuoso/Component/VirtuosoGridPersisted.tsx'; import { VirtuosoGridPersisted } from '@/lib/virtuoso/Component/VirtuosoGridPersisted.tsx';
const GridContainer = React.forwardRef<HTMLDivElement, GridTypeMap['props']>(({ children, ...props }, ref) => ( const GridContainer = ({ children, ref, ...props }: GridTypeMap['props'] & { ref?: Ref<HTMLDivElement> }) => (
<Grid {...props} ref={ref} container spacing={1}> <Grid {...props} ref={ref} container spacing={1}>
{children} {children}
</Grid> </Grid>
)); );
const GridItemContainerWithDimension = ( const GridItemContainerWithDimension = (
dimensions: number, dimensions: number,
@@ -86,115 +77,108 @@ type DefaultGridProps = Pick<MangaCardProps, 'mode'> & {
isSelectModeActive?: boolean; isSelectModeActive?: boolean;
selectedMangaIds?: Required<MangaType['id']>[]; selectedMangaIds?: Required<MangaType['id']>[];
handleSelection?: SelectableCollectionReturnType<MangaType['id']>['handleSelection']; handleSelection?: SelectableCollectionReturnType<MangaType['id']>['handleSelection'];
ref?: ForwardedRef<HTMLDivElement | null>;
}; };
const HorizontalGrid = forwardRef( const HorizontalGrid = ({
( isLoading,
{ mangas,
isLoading, inLibraryIndicator,
mangas, GridItemContainer,
inLibraryIndicator, gridLayout,
GridItemContainer, isSelectModeActive,
gridLayout, selectedMangaIds,
isSelectModeActive, handleSelection,
selectedMangaIds, mode,
handleSelection, ref,
mode, }: DefaultGridProps) => (
}: DefaultGridProps, <Grid
ref: ForwardedRef<HTMLDivElement | null>, ref={ref}
) => ( container
<Grid spacing={1}
ref={ref} sx={{
container width: '100%',
spacing={1} overflowX: 'auto',
sx={{ display: '-webkit-inline-box',
width: '100%', flexWrap: 'nowrap',
overflowX: 'auto', }}
display: '-webkit-inline-box', >
flexWrap: 'nowrap', {isLoading ? (
}} <LoadingPlaceholder />
> ) : (
{isLoading ? ( mangas.map((manga) => (
<LoadingPlaceholder /> <GridItemContainer key={manga.id}>
) : ( {createMangaCard(
mangas.map((manga) => ( manga,
<GridItemContainer key={manga.id}> gridLayout,
{createMangaCard( inLibraryIndicator,
manga, isSelectModeActive,
gridLayout, selectedMangaIds,
inLibraryIndicator, handleSelection,
isSelectModeActive, mode,
selectedMangaIds, )}
handleSelection, </GridItemContainer>
mode, ))
)} )}
</GridItemContainer> </Grid>
))
)}
</Grid>
),
); );
export const MANGA_GRID_SNAPSHOT_KEY = 'MangaGrid-snapshot-location'; export const MANGA_GRID_SNAPSHOT_KEY = 'MangaGrid-snapshot-location';
const VerticalGrid = forwardRef( const VerticalGrid = ({
( isLoading,
{ mangas,
isLoading, inLibraryIndicator,
mangas, GridItemContainer,
inLibraryIndicator, gridLayout,
GridItemContainer, hasNextPage,
gridLayout, loadMore,
hasNextPage, isSelectModeActive,
loadMore, selectedMangaIds,
isSelectModeActive, handleSelection,
selectedMangaIds, mode,
handleSelection, ref,
mode, }: DefaultGridProps & {
}: DefaultGridProps & { hasNextPage: boolean;
hasNextPage: boolean; loadMore: () => void;
loadMore: () => void; }) => (
}, <>
ref: ForwardedRef<HTMLDivElement | null>, <Box ref={ref}>
) => ( <VirtuosoGridPersisted
<> persistKey={MANGA_GRID_SNAPSHOT_KEY}
<Box ref={ref}> useWindowScroll
<VirtuosoGridPersisted increaseViewportBy={window.innerHeight * 0.5}
persistKey={MANGA_GRID_SNAPSHOT_KEY} totalCount={mangas.length}
useWindowScroll components={{
increaseViewportBy={window.innerHeight * 0.5} List: GridContainer,
totalCount={mangas.length} Item: GridItemContainer,
components={{ }}
List: GridContainer, endReached={() => loadMore()}
Item: GridItemContainer, computeItemKey={(index) => mangas[index].id}
}} itemContent={(index) =>
endReached={() => loadMore()} createMangaCard(
computeItemKey={(index) => mangas[index].id} mangas[index],
itemContent={(index) => gridLayout,
createMangaCard( inLibraryIndicator,
mangas[index], isSelectModeActive,
gridLayout, selectedMangaIds,
inLibraryIndicator, handleSelection,
isSelectModeActive, mode,
selectedMangaIds, )
handleSelection, }
mode, />
) </Box>
} {/* render div to prevent UI jumping around when showing/hiding loading placeholder */
/> /* eslint-disable-next-line no-nested-ternary */}
</Box> {isSelectModeActive && gridLayout === GridLayout.List ? (
{/* render div to prevent UI jumping around when showing/hiding loading placeholder */ <Box sx={{ paddingBottom: DEFAULT_FULL_FAB_HEIGHT }} />
/* eslint-disable-next-line no-nested-ternary */} ) : // eslint-disable-next-line no-nested-ternary
{isSelectModeActive && gridLayout === GridLayout.List ? ( isLoading ? (
<Box sx={{ paddingBottom: DEFAULT_FULL_FAB_HEIGHT }} /> <LoadingPlaceholder />
) : // eslint-disable-next-line no-nested-ternary ) : hasNextPage ? (
isLoading ? ( <div style={{ height: '75px' }} />
<LoadingPlaceholder /> ) : null}
) : hasNextPage ? ( </>
<div style={{ height: '75px' }} />
) : null}
</>
),
); );
export interface IMangaGridProps export interface IMangaGridProps

View File

@@ -7,7 +7,7 @@
*/ */
import { useTranslation } from 'react-i18next'; 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 Button from '@mui/material/Button';
import Checkbox from '@mui/material/Checkbox'; import Checkbox from '@mui/material/Checkbox';
import IconButton from '@mui/material/IconButton'; 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 { MangaType } from '@/lib/graphql/generated/graphql.ts';
import { MUIUtil } from '@/lib/mui/MUI.util.ts'; import { MUIUtil } from '@/lib/mui/MUI.util.ts';
export const MangaOptionButton = forwardRef( export const MangaOptionButton = ({
( id,
{ selected,
id, handleSelection,
selected, asCheckbox = false,
handleSelection, popupState,
asCheckbox = false, ref,
popupState, }: {
}: { id: number;
id: number; selected?: boolean | null;
selected?: boolean | null; handleSelection?: SelectableCollectionReturnType<MangaType['id']>['handleSelection'];
handleSelection?: SelectableCollectionReturnType<MangaType['id']>['handleSelection']; asCheckbox?: boolean;
asCheckbox?: boolean; popupState: PopupState;
popupState: PopupState; ref?: ForwardedRef<HTMLButtonElement | null>;
}, }) => {
ref: ForwardedRef<HTMLButtonElement | null>, const { t } = useTranslation();
) => {
const { t } = useTranslation();
const bindTriggerProps = useMemo(() => bindTrigger(popupState), [popupState]); const bindTriggerProps = useMemo(() => bindTrigger(popupState), [popupState]);
const preventDefaultAction = (e: BaseSyntheticEvent) => { const preventDefaultAction = (e: BaseSyntheticEvent) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
}; };
const handleSelectionChange = (e: ChangeEvent, isSelected: boolean) => { const handleSelectionChange = (e: ChangeEvent, isSelected: boolean) => {
preventDefaultAction(e); preventDefaultAction(e);
handleSelection?.(id, isSelected); handleSelection?.(id, isSelected);
}; };
if (!handleSelection) { if (!handleSelection) {
return null;
}
const isSelected = selected !== null;
if (isSelected) {
if (!asCheckbox) {
return null; return null;
} }
const isSelected = selected !== null;
if (isSelected) {
if (!asCheckbox) {
return null;
}
return (
<CustomTooltip title={t(selected ? 'global.button.deselect' : 'global.button.select')}>
<Checkbox {...MUIUtil.preventRippleProp()} checked={selected} onChange={handleSelectionChange} />
</CustomTooltip>
);
}
if (asCheckbox) {
return (
<CustomTooltip title={t('global.button.options')}>
<IconButton
ref={ref}
{...MUIUtil.preventRippleProp(bindTriggerProps, { onClick: preventDefaultAction })}
aria-label="more"
>
<MoreVertIcon />
</IconButton>
</CustomTooltip>
);
}
return ( return (
<CustomTooltip title={t('global.button.options')}> <CustomTooltip title={t(selected ? 'global.button.deselect' : 'global.button.select')}>
<Button <Checkbox {...MUIUtil.preventRippleProp()} checked={selected} onChange={handleSelectionChange} />
ref={ref}
{...MUIUtil.preventRippleProp(bindTriggerProps, { onClick: preventDefaultAction })}
className="manga-option-button"
size="small"
variant="contained"
sx={{
minWidth: 'unset',
paddingX: '0',
paddingY: '2.5px',
visibility: popupState.isOpen ? 'visible' : 'hidden',
pointerEvents: 'none',
'@media not (pointer: fine)': {
visibility: 'hidden',
width: 0,
height: 0,
p: 0,
m: 0,
},
}}
>
<MoreVertIcon />
</Button>
</CustomTooltip> </CustomTooltip>
); );
}, }
);
if (asCheckbox) {
return (
<CustomTooltip title={t('global.button.options')}>
<IconButton
ref={ref}
{...MUIUtil.preventRippleProp(bindTriggerProps, { onClick: preventDefaultAction })}
aria-label="more"
>
<MoreVertIcon />
</IconButton>
</CustomTooltip>
);
}
return (
<CustomTooltip title={t('global.button.options')}>
<Button
ref={ref}
{...MUIUtil.preventRippleProp(bindTriggerProps, { onClick: preventDefaultAction })}
className="manga-option-button"
size="small"
variant="contained"
sx={{
minWidth: 'unset',
paddingX: '0',
paddingY: '2.5px',
visibility: popupState.isOpen ? 'visible' : 'hidden',
pointerEvents: 'none',
'@media not (pointer: fine)': {
visibility: 'hidden',
width: 0,
height: 0,
p: 0,
m: 0,
},
}}
>
<MoreVertIcon />
</Button>
</CustomTooltip>
);
};

View File

@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 { TapZoneInvertMode, TapZoneLayouts } from '@/features/reader/tap-zones/TapZoneLayout.types.ts';
import { TChapterReader } from '@/features/chapter/Chapter.types.ts'; import { TChapterReader } from '@/features/chapter/Chapter.types.ts';
import { ReaderService } from '@/features/reader/services/ReaderService.ts'; import { ReaderService } from '@/features/reader/services/ReaderService.ts';
@@ -349,6 +349,7 @@ export interface ReaderPagerProps
isPreloadMode: boolean; isPreloadMode: boolean;
resumeMode: ReaderResumeMode; resumeMode: ReaderResumeMode;
handleAsInitialRender: boolean; handleAsInitialRender: boolean;
ref?: Ref<HTMLDivElement>;
} }
export enum PageInViewportType { export enum PageInViewportType {

View File

@@ -17,7 +17,7 @@ import Link from '@mui/material/Link';
import { Link as RouterLink } from 'react-router-dom'; import { Link as RouterLink } from 'react-router-dom';
import { alpha } from '@mui/material/styles'; import { alpha } from '@mui/material/styles';
import Slide from '@mui/material/Slide'; import Slide from '@mui/material/Slide';
import { forwardRef, memo } from 'react'; import { memo, Ref } from 'react';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx'; import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx';
import { makeToast } from '@/base/utils/Toast.ts'; import { makeToast } from '@/base/utils/Toast.ts';
@@ -38,7 +38,7 @@ import {
const DEFAULT_MANGA = { ...FALLBACK_MANGA, title: '' }; const DEFAULT_MANGA = { ...FALLBACK_MANGA, title: '' };
const BaseReaderOverlayHeaderMobile = forwardRef<HTMLDivElement, MobileHeaderProps>(({ isVisible }, ref) => { const BaseReaderOverlayHeaderMobile = ({ isVisible, ref }: MobileHeaderProps & { ref?: Ref<HTMLDivElement> }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const popupState = usePopupState({ popupId: 'reader-overlay-more-menu', variant: 'popover' }); const popupState = usePopupState({ popupId: 'reader-overlay-more-menu', variant: 'popover' });
const currentChapter = useReaderChaptersStore((state) => state.chapters.currentChapter); const currentChapter = useReaderChaptersStore((state) => state.chapters.currentChapter);
@@ -126,6 +126,6 @@ const BaseReaderOverlayHeaderMobile = forwardRef<HTMLDivElement, MobileHeaderPro
</Stack> </Stack>
</Slide> </Slide>
); );
}); };
export const ReaderOverlayHeaderMobile = memo(BaseReaderOverlayHeaderMobile); export const ReaderOverlayHeaderMobile = memo(BaseReaderOverlayHeaderMobile);

View File

@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 { CacheProvider } from '@emotion/react';
import { ThemeProvider } from '@mui/material/styles'; import { ThemeProvider } from '@mui/material/styles';
import Box, { BoxProps } from '@mui/material/Box'; 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 { DIRECTION_TO_CACHE } from '@/features/theme/ThemeDirectionCache.ts';
import { withPropsFrom } from '@/base/hoc/withPropsFrom.tsx'; import { withPropsFrom } from '@/base/hoc/withPropsFrom.tsx';
const BaseReaderProgressBarDirectionWrapper = forwardRef< const BaseReaderProgressBarDirectionWrapper = ({
HTMLElement, direction,
BoxProps & { ...boxProps
direction: ReturnType<typeof ReaderService.useGetThemeDirection>; }: BoxProps & {
} direction: ReturnType<typeof ReaderService.useGetThemeDirection>;
>(({ direction, ...boxProps }, ref) => { }) => {
const { const {
settings: { customThemes, appTheme, themeMode, shouldUsePureBlackMode }, settings: { customThemes, appTheme, themeMode, shouldUsePureBlackMode },
} = useMetadataServerSettings(); } = useMetadataServerSettings();
@@ -35,11 +35,11 @@ const BaseReaderProgressBarDirectionWrapper = forwardRef<
return ( return (
<CacheProvider value={DIRECTION_TO_CACHE[direction]}> <CacheProvider value={DIRECTION_TO_CACHE[direction]}>
<ThemeProvider theme={readerTheme}> <ThemeProvider theme={readerTheme}>
<Box {...boxProps} ref={ref} dir={direction} /> <Box {...boxProps} dir={direction} />
</ThemeProvider> </ThemeProvider>
</CacheProvider> </CacheProvider>
); );
}); };
export const ReaderProgressBarDirectionWrapper = withPropsFrom( export const ReaderProgressBarDirectionWrapper = withPropsFrom(
BaseReaderProgressBarDirectionWrapper, BaseReaderProgressBarDirectionWrapper,

View File

@@ -6,17 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { import { ForwardedRef, memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
ForwardedRef,
forwardRef,
memo,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
@@ -74,362 +64,356 @@ const READING_MODE_TO_IN_VIEWPORT_TYPE: Record<ReadingMode, PageInViewportType>
[ReadingMode.WEBTOON]: PageInViewportType.Y, [ReadingMode.WEBTOON]: PageInViewportType.Y,
}; };
const BaseReaderViewer = forwardRef( const BaseReaderViewer = ({
( readerNavBarWidth,
{ updateCurrentPageIndex,
readerNavBarWidth, ref,
updateCurrentPageIndex, }: Pick<NavbarContextType, 'readerNavBarWidth'> & {
}: Pick<NavbarContextType, 'readerNavBarWidth'> & { updateCurrentPageIndex: ReturnType<typeof ReaderControls.useUpdateCurrentPageIndex>;
updateCurrentPageIndex: ReturnType<typeof ReaderControls.useUpdateCurrentPageIndex>; ref?: ForwardedRef<HTMLDivElement | null>;
}) => {
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<ReaderOpenChapterLocationState>().state ?? {
resumeMode: ReaderResumeMode.START,
};
const scrollElementRef = useRef<HTMLDivElement | null>(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<HTMLDivElement | null>, useReaderHandlePageSelection(
) => { pageToScrollToIndex,
const { direction: themeDirection } = useTheme(); currentPageIndex,
const isOverlayVisible = useReaderOverlayStore((state) => state.overlay.isVisible); pages,
const { totalPages,
currentPageIndex, setPageToScrollToIndex,
pageToScrollToIndex, updateCurrentPageIndex,
setPageToScrollToIndex, isContinuousReadingModeActive,
pages, imageRefs,
totalPages, themeDirection,
setPages, readingDirection,
setPageLoadStates, );
setTotalPages, useReaderScrollToStartOnPageChange(
setCurrentPageIndex, currentPageIndex,
transitionPageMode, isContinuousReadingModeActive,
retryFailedPagesKeyPrefix, themeDirection,
setTransitionPageMode, readingDirection,
} = useReaderPagesStore((state) => ({ scrollElementRef,
currentPageIndex: state.pages.currentPageIndex, );
pageToScrollToIndex: state.pages.pageToScrollToIndex, useReaderHideCursorOnInactivity(scrollElementRef);
setPageToScrollToIndex: state.pages.setPageToScrollToIndex, useReaderHorizontalModeInvertXYScrolling(readingMode, readingDirection, scrollElementRef);
pages: state.pages.pages, useReaderHideOverlayOnUserScroll(isOverlayVisible, showPreview, setShowPreview, scrollElementRef);
totalPages: state.pages.totalPages, useReaderAutoScroll(
setPages: state.pages.setPages, isOverlayVisible,
setPageLoadStates: state.pages.setPageLoadStates, automaticScrolling.isPaused,
setTotalPages: state.pages.setTotalPages, automaticScrolling.pause,
setCurrentPageIndex: state.pages.setCurrentPageIndex, automaticScrolling.resume,
transitionPageMode: state.pages.transitionPageMode, isStaticNav,
retryFailedPagesKeyPrefix: state.pages.retryFailedPagesKeyPrefix, );
setTransitionPageMode: state.pages.setTransitionPageMode, useReaderPreserveScrollPosition(
})); scrollElementRef,
const { initialChapter, currentChapter, chapters, visibleChapters, isCurrentChapterReady } = currentChapter?.id,
useReaderChaptersStore((state) => ({ currentChapterIndex,
initialChapter: state.chapters.initialChapter, currentPageIndex,
currentChapter: state.chapters.currentChapter, chaptersToRender,
chapters: state.chapters.chapters, visibleChapters,
visibleChapters: state.chapters.visibleChapters, readingMode,
setReaderStateChapters: state.chapters.setReaderStateChapters, readingDirection,
isCurrentChapterReady: state.chapters.isCurrentChapterReady, setPageToScrollToIndex,
})); pageScaleMode,
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<ReaderOpenChapterLocationState>().state ?? {
resumeMode: ReaderResumeMode.START,
};
const scrollElementRef = useRef<HTMLDivElement | null>(null); useLayoutEffect(() => {
const mergedRef = useMergedRef(ref, scrollElementRef); setChapterViewerSize({
minChapterViewWidth: 0,
minChapterViewHeight: 0,
minChapterSizeSourceChapterId: -1,
});
setTriggerReRender({});
}, [readingMode]);
const isContinuousVerticalReadingModeActive = isContinuousVerticalReadingMode(readingMode); if (!initialChapter || !currentChapter) {
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode); throw new Error('ReaderViewer: illegal state - initialChapter and currentChapter should not be undefined');
const isDragging = useMouseDragScroll(scrollElementRef); }
const automaticScrolling = useReaderAutoScrollStore((state) => ({ return (
isPaused: state.autoScroll.isPaused, <Stack
pause: state.autoScroll.pause, ref={mergedRef}
resume: state.autoScroll.resume, sx={{
setScrollRef: state.autoScroll.setScrollRef, width: '100%',
})); height: '100%',
useEffect(() => automaticScrolling.setScrollRef(scrollElementRef.current), []); overflow: 'auto',
flexWrap: 'nowrap',
const scrollbarXSize = MediaQuery.useGetScrollbarSize('width', scrollElementRef.current); ...applyStyles(
const scrollbarYSize = MediaQuery.useGetScrollbarSize('height', scrollElementRef.current); isContinuousVerticalReadingModeActive && shouldApplyReaderWidth(readerWidth, pageScaleMode),
useLayoutEffect(() => { { alignItems: 'center' },
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], ...applyStyles(!isContinuousVerticalReadingModeActive, {
); ...applyStyles(themeDirection === 'ltr', {
const currentChapterIndex = useMemo( flexDirection: isLtrReadingDirection ? 'row' : 'row-reverse',
() => 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 (
<Stack
ref={mergedRef}
sx={{
width: '100%',
height: '100%',
overflow: 'auto',
flexWrap: 'nowrap',
...applyStyles(
isContinuousVerticalReadingModeActive && shouldApplyReaderWidth(readerWidth, pageScaleMode),
{ alignItems: 'center' },
),
...applyStyles(!isContinuousVerticalReadingModeActive, {
...applyStyles(themeDirection === 'ltr', {
flexDirection: isLtrReadingDirection ? 'row' : 'row-reverse',
}),
...applyStyles(themeDirection === 'rtl', {
flexDirection: isLtrReadingDirection ? 'row-reverse' : 'row',
}),
}), }),
}} ...applyStyles(themeDirection === 'rtl', {
onClick={(e) => !isDragging && ReaderControls.handleClick(scrollElementRef.current, e)} flexDirection: isLtrReadingDirection ? 'row-reverse' : 'row',
onScroll={() => }),
ReaderControls.updateCurrentPageOnScroll( }),
imageRefs, }}
totalPages - 1, onClick={(e) => !isDragging && ReaderControls.handleClick(scrollElementRef.current, e)}
updateCurrentPageIndex, onScroll={() =>
inViewportType, ReaderControls.updateCurrentPageOnScroll(
readingDirection, imageRefs,
) totalPages - 1,
} updateCurrentPageIndex,
> inViewportType,
{chaptersToRender.map((_, index) => { readingDirection,
// 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]; >
{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 = const previousChapter =
chaptersToRender[chapterIndex + 1] ?? chaptersToRender[chapterIndex + 1] ?? chapters[initialChapterIndex + visibleChapters.leading + 1];
chapters[initialChapterIndex + visibleChapters.leading + 1]; const nextChapter =
const nextChapter = chaptersToRender[chapterIndex - 1] ?? chapters[initialChapterIndex - visibleChapters.trailing - 1];
chaptersToRender[chapterIndex - 1] ??
chapters[initialChapterIndex - visibleChapters.trailing - 1];
const isInitialChapter = chapter.id === initialChapter.id; const isInitialChapter = chapter.id === initialChapter.id;
const isCurrentChapter = chapter.id === currentChapter.id; const isCurrentChapter = chapter.id === currentChapter.id;
const isPreviousChapter = chapter.id === chaptersToRender[currentChapterIndex + 1]?.id; const isPreviousChapter = chapter.id === chaptersToRender[currentChapterIndex + 1]?.id;
const isNextChapter = chapter.id === chaptersToRender[currentChapterIndex - 1]?.id; const isNextChapter = chapter.id === chaptersToRender[currentChapterIndex - 1]?.id;
const isLeadingChapter = initialChapter.sourceOrder > chapter.sourceOrder; const isLeadingChapter = initialChapter.sourceOrder > chapter.sourceOrder;
const isTrailingChapter = initialChapter.sourceOrder < chapter.sourceOrder; const isTrailingChapter = initialChapter.sourceOrder < chapter.sourceOrder;
const isLastLeadingChapter = visibleChapters.lastLeadingChapterSourceOrder === chapter.sourceOrder; const isLastLeadingChapter = visibleChapters.lastLeadingChapterSourceOrder === chapter.sourceOrder;
const isLastTrailingChapter = const isLastTrailingChapter = visibleChapters.lastTrailingChapterSourceOrder === chapter.sourceOrder;
visibleChapters.lastTrailingChapterSourceOrder === chapter.sourceOrder; const isPreloadMode =
const isPreloadMode = (isLastLeadingChapter && visibleChapters.isLeadingChapterPreloadMode) ||
(isLastLeadingChapter && visibleChapters.isLeadingChapterPreloadMode) || (isLastTrailingChapter && visibleChapters.isTrailingChapterPreloadMode);
(isLastTrailingChapter && visibleChapters.isTrailingChapterPreloadMode);
const previousNextChapterVisibility = getPreviousNextChapterVisibility( const previousNextChapterVisibility = getPreviousNextChapterVisibility(
chapterIndex, chapterIndex,
chaptersToRender, chaptersToRender,
visibleChapters, visibleChapters,
); );
const isChapterSizeSourceChapter = chapter.id === minChapterSizeSourceChapterId; const isChapterSizeSourceChapter = chapter.id === minChapterSizeSourceChapterId;
return ( return (
<ReaderChapterViewer <ReaderChapterViewer
key={chapter.id} key={chapter.id}
chapterId={chapter.id} chapterId={chapter.id}
previousChapterId={previousChapter?.id} previousChapterId={previousChapter?.id}
nextChapterId={nextChapter?.id} nextChapterId={nextChapter?.id}
isPreviousChapterVisible={previousNextChapterVisibility.previous} isPreviousChapterVisible={previousNextChapterVisibility.previous}
isNextChapterVisible={previousNextChapterVisibility.next} isNextChapterVisible={previousNextChapterVisibility.next}
lastPageRead={coerceIn(chapter.lastPageRead, 0, chapter.pageCount - 1)} lastPageRead={coerceIn(chapter.lastPageRead, 0, chapter.pageCount - 1)}
currentPageIndex={getReaderChapterViewerCurrentPageIndex( currentPageIndex={getReaderChapterViewerCurrentPageIndex(
currentPageIndex, currentPageIndex,
chapter, chapter,
currentChapter, currentChapter,
isCurrentChapter, isCurrentChapter,
isCurrentChapterReady, isCurrentChapterReady,
isLeadingChapter, isLeadingChapter,
isTrailingChapter, isTrailingChapter,
visibleChapters, visibleChapters,
)} )}
isInitialChapter={isInitialChapter} isInitialChapter={isInitialChapter}
isCurrentChapter={isCurrentChapter} isCurrentChapter={isCurrentChapter}
isPreviousChapter={isPreviousChapter} isPreviousChapter={isPreviousChapter}
isNextChapter={isNextChapter} isNextChapter={isNextChapter}
isLeadingChapter={isLeadingChapter} isLeadingChapter={isLeadingChapter}
isTrailingChapter={isTrailingChapter} isTrailingChapter={isTrailingChapter}
isPreloadMode={isPreloadMode} isPreloadMode={isPreloadMode}
imageRefs={imageRefs} imageRefs={imageRefs}
setPages={setPages} setPages={setPages}
setPageLoadStates={setPageLoadStates} setPageLoadStates={setPageLoadStates}
setTotalPages={setTotalPages} setTotalPages={setTotalPages}
setCurrentPageIndex={setCurrentPageIndex} setCurrentPageIndex={setCurrentPageIndex}
setPageToScrollToIndex={setPageToScrollToIndex} setPageToScrollToIndex={setPageToScrollToIndex}
transitionPageMode={transitionPageMode} transitionPageMode={transitionPageMode}
retryFailedPagesKeyPrefix={retryFailedPagesKeyPrefix} retryFailedPagesKeyPrefix={retryFailedPagesKeyPrefix}
readingMode={readingMode} readingMode={readingMode}
readerWidth={readerWidth} readerWidth={readerWidth}
pageScaleMode={pageScaleMode} pageScaleMode={pageScaleMode}
shouldOffsetDoubleSpreads={shouldOffsetDoubleSpreads} shouldOffsetDoubleSpreads={shouldOffsetDoubleSpreads}
readingDirection={readingDirection} readingDirection={readingDirection}
updateCurrentPageIndex={isCurrentChapter ? updateCurrentPageIndex : noOp} updateCurrentPageIndex={isCurrentChapter ? updateCurrentPageIndex : noOp}
scrollIntoView={isCurrentChapter && visibleChapters.scrollIntoView} scrollIntoView={isCurrentChapter && visibleChapters.scrollIntoView}
resumeMode={getReaderChapterViewResumeMode( resumeMode={getReaderChapterViewResumeMode(
isCurrentChapter, isCurrentChapter,
isInitialChapter, isInitialChapter,
isLeadingChapter, isLeadingChapter,
isTrailingChapter, isTrailingChapter,
visibleChapters.resumeMode, visibleChapters.resumeMode,
resumeMode, resumeMode,
)} )}
setTransitionPageMode={setTransitionPageMode} setTransitionPageMode={setTransitionPageMode}
pageGap={pageGap} pageGap={pageGap}
imagePreLoadAmount={imagePreLoadAmount} imagePreLoadAmount={imagePreLoadAmount}
customFilter={customFilter} customFilter={customFilter}
shouldStretchPage={shouldStretchPage} shouldStretchPage={shouldStretchPage}
readerNavBarWidth={readerNavBarWidth} readerNavBarWidth={readerNavBarWidth}
onSizeChange={onChapterViewSizeChange} onSizeChange={onChapterViewSizeChange}
minWidth={isChapterSizeSourceChapter ? 0 : minChapterViewWidth} minWidth={isChapterSizeSourceChapter ? 0 : minChapterViewWidth}
minHeight={isChapterSizeSourceChapter ? 0 : minChapterViewHeight} minHeight={isChapterSizeSourceChapter ? 0 : minChapterViewHeight}
scrollElement={scrollElementRef.current} scrollElement={scrollElementRef.current}
/> />
); );
})} })}
</Stack> </Stack>
); );
}, };
);
export const ReaderViewer = withPropsFrom( export const ReaderViewer = withPropsFrom(
memo(BaseReaderViewer), memo(BaseReaderViewer),

View File

@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 Box, { BoxProps } from '@mui/material/Box';
import { getPageIndexesToLoad, isATransitionPageVisible } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx'; import { getPageIndexesToLoad, isATransitionPageVisible } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
import { import {
@@ -22,133 +22,117 @@ import { isContinuousReadingMode } from '@/features/reader/settings/ReaderSettin
const getPreviousCurrentPageIndex = (resumeMode: ReaderResumeMode): number => const getPreviousCurrentPageIndex = (resumeMode: ReaderResumeMode): number =>
resumeMode === ReaderResumeMode.END ? Number.MAX_SAFE_INTEGER : -1; resumeMode === ReaderResumeMode.END ? Number.MAX_SAFE_INTEGER : -1;
const BaseBasePager = forwardRef< const BaseBasePager = ({
HTMLDivElement, currentPageIndex,
Omit<ReaderPagerProps, 'pageLoadStates' | 'retryFailedPagesKeyPrefix' | 'isPreloadMode'> & pages,
Pick<IReaderSettings, 'readingMode' | 'imagePreLoadAmount'> & { transitionPageMode,
createPage: ( imageRefs,
page: ReaderStatePages['pages'][number], createPage,
pagesIndex: number, slots,
shouldLoad: boolean, readingMode,
shouldDisplay: boolean, imagePreLoadAmount,
setRef: (pagesIndex: number, element: HTMLElement | null) => void, isCurrentChapter,
readingMode: ReaderPagerProps['readingMode'], isPreviousChapter,
customFilter: ReaderPagerProps['customFilter'], isNextChapter,
pageScaleMode: ReaderPagerProps['pageScaleMode'], customFilter,
shouldStretchPage: ReaderPagerProps['shouldStretchPage'], pageScaleMode,
readerWidth: ReaderPagerProps['readerWidth'], shouldStretchPage,
readerNavBarWidth: ReaderPagerProps['readerNavBarWidth'], readerWidth,
) => ReactNode; readerNavBarWidth,
slots?: { boxProps?: BoxProps }; resumeMode,
} handleAsInitialRender,
>( ref,
( }: Omit<ReaderPagerProps, 'pageLoadStates' | 'retryFailedPagesKeyPrefix' | 'isPreloadMode'> &
{ Pick<IReaderSettings, 'readingMode' | 'imagePreLoadAmount'> & {
currentPageIndex, createPage: (
pages, page: ReaderStatePages['pages'][number],
transitionPageMode, pagesIndex: number,
imageRefs, shouldLoad: boolean,
createPage, shouldDisplay: boolean,
slots, setRef: (pagesIndex: number, element: HTMLElement | null) => void,
readingMode, readingMode: ReaderPagerProps['readingMode'],
imagePreLoadAmount, customFilter: ReaderPagerProps['customFilter'],
isCurrentChapter, pageScaleMode: ReaderPagerProps['pageScaleMode'],
isPreviousChapter, shouldStretchPage: ReaderPagerProps['shouldStretchPage'],
isNextChapter, readerWidth: ReaderPagerProps['readerWidth'],
customFilter, readerNavBarWidth: ReaderPagerProps['readerNavBarWidth'],
pageScaleMode, ) => ReactNode;
shouldStretchPage, slots?: { boxProps?: BoxProps };
readerWidth, }) => {
readerNavBarWidth, const previousCurrentPageIndex = useRef(getPreviousCurrentPageIndex(resumeMode));
resumeMode,
handleAsInitialRender,
},
ref,
) => {
const previousCurrentPageIndex = useRef(getPreviousCurrentPageIndex(resumeMode));
if (handleAsInitialRender) { if (handleAsInitialRender) {
previousCurrentPageIndex.current = getPreviousCurrentPageIndex(resumeMode); previousCurrentPageIndex.current = getPreviousCurrentPageIndex(resumeMode);
} }
const pagesIndexesToRender = useMemo( const pagesIndexesToRender = useMemo(
() => () =>
getPageIndexesToLoad( getPageIndexesToLoad(
currentPageIndex,
pages,
previousCurrentPageIndex.current,
imagePreLoadAmount,
readingMode,
isCurrentChapter,
isPreviousChapter,
isNextChapter,
),
[
currentPageIndex, currentPageIndex,
pages, pages,
previousCurrentPageIndex.current,
imagePreLoadAmount, imagePreLoadAmount,
readingMode, readingMode,
isCurrentChapter, isCurrentChapter,
isPreviousChapter, isPreviousChapter,
isNextChapter, isNextChapter,
], ),
); [currentPageIndex, pages, imagePreLoadAmount, readingMode, isCurrentChapter, isPreviousChapter, isNextChapter],
useEffect(() => { );
if (isCurrentChapter) { useEffect(() => {
previousCurrentPageIndex.current = currentPageIndex; if (isCurrentChapter) {
} previousCurrentPageIndex.current = currentPageIndex;
}, [pagesIndexesToRender, isCurrentChapter]); }
}, [pagesIndexesToRender, isCurrentChapter]);
const setRef = useCallback( const setRef = useCallback(
(pagesIndex: number, element: HTMLElement | null) => { (pagesIndex: number, element: HTMLElement | null) => {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
imageRefs.current[pagesIndex] = element; imageRefs.current[pagesIndex] = element;
}, },
[imageRefs], [imageRefs],
); );
return ( return (
<Box <Box
ref={ref} ref={ref}
{...slots?.boxProps} {...slots?.boxProps}
sx={[ sx={[
{
width: 'fit-content',
height: 'fit-content',
},
...(Array.isArray(slots?.boxProps?.sx) ? (slots?.boxProps?.sx ?? []) : [slots?.boxProps?.sx]),
// hide pager, without actually unmounting it to prevent re-renders, while a chapter transition page is taking up the full screen
applyStyles(
!isContinuousReadingMode(readingMode) && isATransitionPageVisible(transitionPageMode, readingMode),
{ {
width: 'fit-content', visibility: 'hidden',
height: 'fit-content', width: 0,
height: 0,
m: 0,
p: 0,
}, },
...(Array.isArray(slots?.boxProps?.sx) ? (slots?.boxProps?.sx ?? []) : [slots?.boxProps?.sx]), ),
// hide pager, without actually unmounting it to prevent re-renders, while a chapter transition page is taking up the full screen ]}
applyStyles( >
!isContinuousReadingMode(readingMode) && {pages.map((page, pagesIndex) =>
isATransitionPageVisible(transitionPageMode, readingMode), createPage(
{ page,
visibility: 'hidden', pagesIndex,
width: 0, pagesIndexesToRender.includes(pagesIndex),
height: 0, [ReaderTransitionPageMode.NONE, ReaderTransitionPageMode.BOTH].includes(transitionPageMode),
m: 0, setRef,
p: 0, readingMode,
}, customFilter,
), pageScaleMode,
]} shouldStretchPage,
> readerWidth,
{pages.map((page, pagesIndex) => readerNavBarWidth,
createPage( ),
page, )}
pagesIndex, </Box>
pagesIndexesToRender.includes(pagesIndex), );
[ReaderTransitionPageMode.NONE, ReaderTransitionPageMode.BOTH].includes(transitionPageMode), };
setRef,
readingMode,
customFilter,
pageScaleMode,
shouldStretchPage,
readerWidth,
readerNavBarWidth,
),
)}
</Box>
);
},
);
export const BasePager = memo(BaseBasePager); export const BasePager = memo(BaseBasePager);

View File

@@ -7,7 +7,7 @@
*/ */
import { Direction, useTheme } from '@mui/material/styles'; 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 { BasePager } from '@/features/reader/viewer/pager/components/BasePager.tsx';
import { import {
IReaderSettings, IReaderSettings,
@@ -41,10 +41,14 @@ const getPagePosition = (
return isLtrReadingDirection ? 'right' : 'left'; return isLtrReadingDirection ? 'right' : 'left';
}; };
const BaseReaderDoublePagedPager = forwardRef< const BaseReaderDoublePagedPager = ({
HTMLDivElement, onLoad,
ReaderPagerProps & Pick<IReaderSettings, 'readingDirection' | 'pageScaleMode'> onError,
>(({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => { pageLoadStates,
retryFailedPagesKeyPrefix,
isPreloadMode,
...props
}: ReaderPagerProps & Pick<IReaderSettings, 'readingDirection' | 'pageScaleMode'>) => {
const { currentPageIndex, pages, totalPages, readingDirection, pageScaleMode } = props; const { currentPageIndex, pages, totalPages, readingDirection, pageScaleMode } = props;
const { direction: themeDirection } = useTheme(); const { direction: themeDirection } = useTheme();
@@ -54,7 +58,6 @@ const BaseReaderDoublePagedPager = forwardRef<
return ( return (
<BasePager <BasePager
ref={ref}
{...props} {...props}
createPage={(page, pagesIndex, shouldLoad, shouldDisplay, _setRef, ...baseProps) => { createPage={(page, pagesIndex, shouldLoad, shouldDisplay, _setRef, ...baseProps) => {
const { primary, secondary } = page; const { primary, secondary } = page;
@@ -129,6 +132,6 @@ const BaseReaderDoublePagedPager = forwardRef<
}} }}
/> />
); );
}); };
export const ReaderDoublePagedPager = memo(BaseReaderDoublePagedPager); export const ReaderDoublePagedPager = memo(BaseReaderDoublePagedPager);

View File

@@ -7,16 +7,20 @@
*/ */
import { useTheme } from '@mui/material/styles'; 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 { BasePager } from '@/features/reader/viewer/pager/components/BasePager.tsx';
import { applyStyles } from '@/base/utils/ApplyStyles.ts'; import { applyStyles } from '@/base/utils/ApplyStyles.ts';
import { IReaderSettings, ReaderPagerProps, ReadingDirection } from '@/features/reader/Reader.types.ts'; import { IReaderSettings, ReaderPagerProps, ReadingDirection } from '@/features/reader/Reader.types.ts';
import { createReaderPage } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx'; import { createReaderPage } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
const BaseReaderHorizontalPager = forwardRef< const BaseReaderHorizontalPager = ({
HTMLDivElement, onLoad,
ReaderPagerProps & Pick<IReaderSettings, 'pageGap' | 'readingDirection'> onError,
>(({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => { pageLoadStates,
retryFailedPagesKeyPrefix,
isPreloadMode,
...props
}: ReaderPagerProps & Pick<IReaderSettings, 'pageGap' | 'readingDirection'>) => {
const { currentPageIndex, totalPages, pageGap, readingDirection } = props; const { currentPageIndex, totalPages, pageGap, readingDirection } = props;
const { direction: themeDirection } = useTheme(); const { direction: themeDirection } = useTheme();
@@ -25,7 +29,6 @@ const BaseReaderHorizontalPager = forwardRef<
return ( return (
<BasePager <BasePager
ref={ref}
{...props} {...props}
createPage={(page, pagesIndex, shouldLoad, _, setRef, ...baseProps) => createPage={(page, pagesIndex, shouldLoad, _, setRef, ...baseProps) =>
createReaderPage( createReaderPage(
@@ -69,6 +72,6 @@ const BaseReaderHorizontalPager = forwardRef<
}} }}
/> />
); );
}); };
export const ReaderHorizontalPager = memo(BaseReaderHorizontalPager); export const ReaderHorizontalPager = memo(BaseReaderHorizontalPager);

View File

@@ -6,46 +6,50 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 { BasePager } from '@/features/reader/viewer/pager/components/BasePager.tsx';
import { ReaderPagerProps } from '@/features/reader/Reader.types.ts'; import { ReaderPagerProps } from '@/features/reader/Reader.types.ts';
import { createReaderPage } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx'; import { createReaderPage } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
const BaseReaderPagedPager = forwardRef<HTMLDivElement, ReaderPagerProps>( const BaseReaderPagedPager = ({
({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => { onLoad,
const { currentPageIndex, totalPages } = props; onError,
pageLoadStates,
retryFailedPagesKeyPrefix,
isPreloadMode,
...props
}: ReaderPagerProps) => {
const { currentPageIndex, totalPages } = props;
return ( return (
<BasePager <BasePager
ref={ref} {...props}
{...props} createPage={(page, pagesIndex, shouldLoad, shouldDisplay, _setRef, ...baseProps) =>
createPage={(page, pagesIndex, shouldLoad, shouldDisplay, _setRef, ...baseProps) => createReaderPage(
createReaderPage( page,
page, pagesIndex,
pagesIndex, true,
true, pageLoadStates[page.primary.index].loaded,
pageLoadStates[page.primary.index].loaded, isPreloadMode,
isPreloadMode, onLoad,
onLoad, onError,
onError, shouldLoad,
shouldLoad, shouldDisplay && shouldLoad && currentPageIndex === page.primary.index,
shouldDisplay && shouldLoad && currentPageIndex === page.primary.index, currentPageIndex,
currentPageIndex, totalPages,
totalPages, ...baseProps,
...baseProps, pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined,
pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined, )
) }
} slots={{
slots={{ boxProps: {
boxProps: { sx: {
sx: { margin: 'auto',
margin: 'auto',
},
}, },
}} },
/> }}
); />
}, );
); };
export const ReaderPagedPager = memo(BaseReaderPagedPager); export const ReaderPagedPager = memo(BaseReaderPagedPager);

View File

@@ -6,47 +6,51 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 { BasePager } from '@/features/reader/viewer/pager/components/BasePager.tsx';
import { ReaderPagerProps, ReadingMode } from '@/features/reader/Reader.types.ts'; import { ReaderPagerProps, ReadingMode } from '@/features/reader/Reader.types.ts';
import { createReaderPage } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx'; import { createReaderPage } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
const BaseReaderVerticalPager = forwardRef<HTMLDivElement, ReaderPagerProps>( const BaseReaderVerticalPager = ({
({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => { onLoad,
const { currentPageIndex, totalPages, readingMode, pageGap } = props; onError,
pageLoadStates,
retryFailedPagesKeyPrefix,
isPreloadMode,
...props
}: ReaderPagerProps) => {
const { currentPageIndex, totalPages, readingMode, pageGap } = props;
const isWebtoonMode = readingMode === ReadingMode.WEBTOON; const isWebtoonMode = readingMode === ReadingMode.WEBTOON;
const actualPageGap = isWebtoonMode ? 0 : pageGap; const actualPageGap = isWebtoonMode ? 0 : pageGap;
return ( return (
<BasePager <BasePager
ref={ref} {...props}
{...props} createPage={(page, pagesIndex, shouldLoad, _, setRef, ...baseProps) =>
createPage={(page, pagesIndex, shouldLoad, _, setRef, ...baseProps) => createReaderPage(
createReaderPage( page,
page, pagesIndex,
pagesIndex, true,
true, pageLoadStates[page.primary.index].loaded,
pageLoadStates[page.primary.index].loaded, isPreloadMode,
isPreloadMode, onLoad,
onLoad, onError,
onError, shouldLoad,
shouldLoad, !isPreloadMode,
!isPreloadMode, currentPageIndex,
currentPageIndex, totalPages,
totalPages, ...baseProps,
...baseProps, pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined,
pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined, undefined,
undefined, undefined,
undefined, page.primary.index !== 0 ? actualPageGap : 0,
page.primary.index !== 0 ? actualPageGap : 0, setRef,
setRef, )
) }
} slots={{ boxProps: { sx: { margin: 'auto' } } }}
slots={{ boxProps: { sx: { margin: 'auto' } } }} />
/> );
); };
},
);
export const ReaderVerticalPager = memo(BaseReaderVerticalPager); export const ReaderVerticalPager = memo(BaseReaderVerticalPager);