Remove "forwardRef" usage
This commit is contained in:
@@ -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>
|
||||||
</>
|
)}
|
||||||
);
|
</>
|
||||||
},
|
);
|
||||||
);
|
};
|
||||||
|
|||||||
@@ -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>
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
|
|||||||
@@ -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>
|
);
|
||||||
);
|
};
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -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} />;
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user