Files
suwayomi-material-you-webui/src/features/reader/viewer/components/ReaderTransitionPage.tsx

283 lines
12 KiB
TypeScript
Raw Normal View History

2024-12-03 18:00:46 +01:00
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import { Link } from 'react-router-dom';
import { ComponentProps, memo, useMemo } from 'react';
import { alpha, useTheme } from '@mui/material/styles';
import { useLingui } from '@lingui/react/macro';
import { IReaderSettings, ReaderTransitionPageMode, ReadingMode } from '@/features/reader/Reader.types.ts';
2025-08-16 02:02:13 +02:00
import { isTransitionPageVisible } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
import { useBackButton } from '@/base/hooks/useBackButton.ts';
import { applyStyles } from '@/base/utils/ApplyStyles.ts';
2024-12-16 16:26:32 +01:00
import {
isContinuousReadingMode,
isContinuousVerticalReadingMode,
2025-08-16 02:02:13 +02:00
} from '@/features/reader/settings/ReaderSettings.utils.tsx';
import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
2025-08-15 22:02:58 +02:00
import { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
import { withPropsFrom } from '@/base/hoc/withPropsFrom.tsx';
import { getValueFromObject, noOp } from '@/lib/HelperFunctions.ts';
2025-08-16 02:02:13 +02:00
import { READER_BACKGROUND_TO_COLOR } from '@/features/reader/settings/ReaderSettings.constants.tsx';
import { ChapterType } from '@/lib/graphql/generated/graphql.ts';
2025-08-15 22:02:58 +02:00
import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
import {
useReaderChaptersStore,
useReaderPagesStore,
useReaderScrollbarStore,
useReaderSettingsStore,
useReaderStore,
} from '@/features/reader/stores/ReaderStore.ts';
2024-12-03 18:00:46 +01:00
const ChapterInfo = ({
title,
name,
scanlator,
backgroundColor,
2024-12-03 18:00:46 +01:00
}: {
title: string;
name?: ChapterType['name'];
scanlator?: ChapterType['scanlator'];
backgroundColor: IReaderSettings['backgroundColor'];
2024-12-03 18:00:46 +01:00
}) => {
const theme = useTheme();
const contrastText = theme.palette.getContrastText(
getValueFromObject(theme.palette, READER_BACKGROUND_TO_COLOR[backgroundColor]),
);
const disabledText = alpha(contrastText, 0.5);
if (!name) {
2024-12-03 18:00:46 +01:00
return null;
}
return (
<Stack>
<Typography color={contrastText}>{title}</Typography>
<Typography color={contrastText} variant="h6" component="h1">
{name}
2024-12-03 18:00:46 +01:00
</Typography>
{scanlator && (
<Typography variant="body2" color={disabledText}>
{scanlator}
2024-12-03 18:00:46 +01:00
</Typography>
)}
</Stack>
);
};
2024-12-21 03:46:55 +01:00
const BaseReaderTransitionPage = ({
2024-12-03 18:00:46 +01:00
type,
currentChapterName,
currentChapterScanlator,
previousChapterName,
previousChapterScanlator,
nextChapterName,
nextChapterScanlator,
2024-12-21 03:46:55 +01:00
readerNavBarWidth,
handleBack,
}: Pick<NavbarContextType, 'readerNavBarWidth'> & {
// gets used in the "source props creators" of the "withPropsFrom" call
// eslint-disable-next-line react/no-unused-prop-types
chapterId: ChapterIdInfo['id'];
currentChapterName?: ChapterType['name'];
currentChapterScanlator?: ChapterType['scanlator'];
previousChapterName?: ChapterType['name'];
previousChapterScanlator?: ChapterType['scanlator'];
nextChapterName?: ChapterType['name'];
nextChapterScanlator?: ChapterType['scanlator'];
type: Exclude<ReaderTransitionPageMode, ReaderTransitionPageMode.NONE | ReaderTransitionPageMode.BOTH>;
handleBack: () => void;
}) => {
const { t } = useLingui();
const manga = useReaderStore((state) => state.manga);
2026-02-21 17:33:13 +01:00
const scrollbar = useReaderScrollbarStore((state) => state);
const transitionPageMode = useReaderPagesStore((state) => state.transitionPageMode);
const { readingMode, backgroundColor, shouldShowTransitionPage } = useReaderSettingsStore((state) => ({
2026-02-21 17:33:13 +01:00
readingMode: state.readingMode.value,
backgroundColor: state.backgroundColor,
shouldShowTransitionPage: state.shouldShowTransitionPage,
}));
2024-12-03 18:00:46 +01:00
const isPreviousType = type === ReaderTransitionPageMode.PREVIOUS;
const isNextType = type === ReaderTransitionPageMode.NEXT;
const isFirstChapter = !!currentChapterName && !previousChapterName;
const isLastChapter = !!currentChapterName && !nextChapterName;
2024-12-03 18:00:46 +01:00
const forceShowFirstChapterPreviousTransitionPage = isFirstChapter && type === ReaderTransitionPageMode.PREVIOUS;
const forceShowLastChapterNextTransitionPage = isLastChapter && type === ReaderTransitionPageMode.NEXT;
const forceShowTransitionPage =
forceShowFirstChapterPreviousTransitionPage || forceShowLastChapterNextTransitionPage;
if (!shouldShowTransitionPage && !forceShowTransitionPage) {
return null;
}
if (!isTransitionPageVisible(type, transitionPageMode, readingMode)) {
2024-12-03 18:00:46 +01:00
return null;
}
return (
<Stack
sx={{
justifyContent: 'center',
alignItems: 'center',
...applyStyles(!isContinuousReadingMode(readingMode), {
width: '100%',
height: '100%',
}),
2024-12-03 18:00:46 +01:00
...applyStyles(isContinuousReadingMode(readingMode), {
2025-03-24 23:46:57 +01:00
position: 'sticky',
2024-12-16 16:26:32 +01:00
...applyStyles(isContinuousVerticalReadingMode(readingMode), {
left: 0,
maxWidth: `calc(100vw - ${scrollbar.ySize}px - ${readerNavBarWidth}px)`,
minHeight: `calc(100vh - ${scrollbar.xSize}px)`,
2024-12-03 18:00:46 +01:00
}),
...applyStyles(readingMode === ReadingMode.CONTINUOUS_HORIZONTAL, {
top: 0,
minWidth: `calc(100vw - ${scrollbar.ySize}px - ${readerNavBarWidth}px)`,
maxHeight: `calc(100vh - ${scrollbar.xSize}px)`,
2024-12-03 18:00:46 +01:00
}),
}),
}}
>
<Stack
sx={{
gap: 2,
maxWidth: (theme) =>
// spacing = added padding left + right
`calc(100vw - ${scrollbar.ySize}px - ${readerNavBarWidth}px - ${theme.spacing(2)})`,
maxHeight: `calc(100vh - ${scrollbar.xSize}px)`,
2024-12-03 18:00:46 +01:00
width: 'max-content',
p: 1,
}}
>
{isPreviousType && isFirstChapter && (
<Typography variant="h6">{t`There is no previous chapter`}</Typography>
2024-12-03 18:00:46 +01:00
)}
<Stack sx={{ gap: 5 }}>
{isPreviousType && !isFirstChapter && (
<ChapterInfo
title={t`Previous:`}
name={previousChapterName}
scanlator={previousChapterScanlator}
backgroundColor={backgroundColor}
/>
2024-12-03 18:00:46 +01:00
)}
{!!currentChapterName && (
2024-12-03 18:00:46 +01:00
<ChapterInfo
title={isPreviousType ? t`Current:` : t`Finished:`}
name={currentChapterName}
scanlator={currentChapterScanlator}
backgroundColor={backgroundColor}
2024-12-03 18:00:46 +01:00
/>
)}
{isNextType && !isLastChapter && (
<ChapterInfo
title={t`Next:`}
name={nextChapterName}
scanlator={nextChapterScanlator}
backgroundColor={backgroundColor}
/>
2024-12-03 18:00:46 +01:00
)}
</Stack>
{isNextType && isLastChapter && <Typography variant="h6">{t`There is no next chapter`}</Typography>}
2024-12-03 18:00:46 +01:00
{((isPreviousType && isFirstChapter) || (isNextType && isLastChapter)) && (
<Stack sx={{ flexDirection: 'row', flexWrap: 'wrap', gap: 1 }}>
<Button
sx={{ flexGrow: 1 }}
onClick={(e) => {
e.stopPropagation();
handleBack();
}}
variant="contained"
>
{t`Exit to previous page`}
2024-12-03 18:00:46 +01:00
</Button>
<Button
sx={{ flexGrow: 1 }}
component={Link}
onClick={(e) => {
e.stopPropagation();
}}
variant="contained"
2024-12-11 20:10:59 +01:00
to={AppRoutes.manga.path(manga?.id ?? -1)}
2024-12-03 18:00:46 +01:00
>
{t`Exit to manga page`}
2024-12-03 18:00:46 +01:00
</Button>
</Stack>
)}
</Stack>
</Stack>
);
};
2024-12-21 03:46:55 +01:00
export const ReaderTransitionPage = withPropsFrom(
memo(BaseReaderTransitionPage) as typeof BaseReaderTransitionPage,
[
({ chapterId }: Pick<ComponentProps<typeof BaseReaderTransitionPage>, 'chapterId'>) => {
2026-02-21 17:33:13 +01:00
const chapters = useReaderChaptersStore((state) => state.chapters);
const currentChapterIndex = useMemo(
() => chapters.findIndex((chapter) => chapter.id === chapterId),
[chapterId, chapters],
);
const currentChapter = chapters[currentChapterIndex];
// chapters are sorted from latest to oldest
const previousChapter = useMemo(() => chapters[currentChapterIndex + 1], [currentChapterIndex, chapters]);
const nextChapter = useMemo(() => chapters[currentChapterIndex - 1], [currentChapterIndex, chapters]);
return {
currentChapterName: currentChapter?.name,
currentChapterScanlator: currentChapter?.scanlator,
previousChapterName: previousChapter?.name,
previousChapterScanlator: previousChapter?.name,
nextChapterName: nextChapter?.name,
nextChapterScanlator: nextChapter?.scanlator,
};
},
useNavBarContext,
({ chapterId, type }: Pick<ComponentProps<typeof BaseReaderTransitionPage>, 'chapterId' | 'type'>) => {
const handleBack = useBackButton();
2026-02-21 17:33:13 +01:00
const chapters = useReaderChaptersStore((state) => state.chapters);
const currentChapterIndex = useMemo(
() => chapters.findIndex((chapter) => chapter.id === chapterId),
[chapterId, chapters],
);
// chapters are sorted from latest to oldest
const isLastChapter = currentChapterIndex === 0;
const isFirstChapter = currentChapterIndex === chapters.length - 1;
const handleBackFirstChapter = type === ReaderTransitionPageMode.PREVIOUS && isFirstChapter;
const handleBackLastChapter = type === ReaderTransitionPageMode.NEXT && isLastChapter;
const needsToHandleBack = handleBackFirstChapter || handleBackLastChapter;
return {
handleBack: needsToHandleBack ? handleBack : noOp,
};
},
],
2024-12-21 03:46:55 +01:00
[
'currentChapterName',
'currentChapterScanlator',
'previousChapterName',
'previousChapterScanlator',
'nextChapterName',
'nextChapterScanlator',
2024-12-21 03:46:55 +01:00
'readerNavBarWidth',
'handleBack',
2024-12-21 03:46:55 +01:00
],
);