Files
suwayomi-material-you-webui/src/features/reader/screens/Reader.tsx

347 lines
12 KiB
TypeScript
Raw Normal View History

2024-12-03 20:24:55 +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 Box from '@mui/material/Box';
2025-01-28 02:16:00 +01:00
import { memo, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
2024-12-03 20:24:55 +01:00
import { useTranslation } from 'react-i18next';
2025-08-16 02:02:13 +02:00
import { useDefaultReaderSettings } from '@/features/reader/settings/ReaderSettingsMetadata.ts';
import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx';
2025-08-16 02:02:13 +02:00
import { ReaderOverlay } from '@/features/reader/overlay/ReaderOverlay.tsx';
2024-12-03 20:24:55 +01:00
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { GetChaptersReaderQuery, GetMangaReaderQuery } from '@/lib/graphql/generated/graphql.ts';
import { GET_MANGA_READER } from '@/lib/graphql/queries/MangaQuery.ts';
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx';
2024-12-03 20:24:55 +01:00
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
2025-08-15 22:02:58 +02:00
import { userReaderStatePagesContext } from '@/features/reader/contexts/state/ReaderStatePagesContext.tsx';
2024-12-03 20:24:55 +01:00
import { GET_CHAPTERS_READER } from '@/lib/graphql/queries/ChapterQuery.ts';
2025-08-16 02:02:13 +02:00
import { TapZoneLayout } from '@/features/reader/tap-zones/TapZoneLayout.tsx';
import { ReaderRGBAFilter } from '@/features/reader/filters/ReaderRGBAFilter.tsx';
2025-08-15 22:02:58 +02:00
import { useReaderStateSettingsContext } from '@/features/reader/contexts/state/ReaderStateSettingsContext.tsx';
2025-08-16 02:02:13 +02:00
import { ReaderViewer } from '@/features/reader/viewer/ReaderViewer.tsx';
2025-08-15 22:02:58 +02:00
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
2025-08-16 02:02:13 +02:00
import { READER_BACKGROUND_TO_COLOR } from '@/features/reader/settings/ReaderSettings.constants.tsx';
import { ReaderHotkeys } from '@/features/reader/hotkeys/ReaderHotkeys.tsx';
2024-12-21 03:46:55 +01:00
import {
IReaderSettings,
IReaderSettingsWithDefaultFlag,
2024-12-21 03:46:55 +01:00
ReaderStateChapters,
ReaderStatePages,
2024-12-21 03:46:55 +01:00
TReaderStateSettingsContext,
2025-08-16 02:02:13 +02:00
} from '@/features/reader/Reader.types.ts';
2024-12-21 23:27:03 +01:00
import { getErrorMessage } from '@/lib/HelperFunctions.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';
2025-08-15 22:02:58 +02:00
import { useReaderStateChaptersContext } from '@/features/reader/contexts/state/ReaderStateChaptersContext.tsx';
2025-08-16 02:02:13 +02:00
import { TReaderTapZoneContext } from '@/features/reader/tap-zones/TapZoneLayout.types.ts';
import { useReaderTapZoneContext } from '@/features/reader/tap-zones/ReaderTapZoneContext.tsx';
2025-08-15 22:02:58 +02:00
import { useReaderResetStates } from '@/features/reader/hooks/useReaderResetStates.ts';
import { useReaderSetSettingsState } from '@/features/reader/hooks/useReaderSetSettingsState.ts';
import { useReaderShowSettingPreviewOnChange } from '@/features/reader/hooks/useReaderShowSettingPreviewOnChange.ts';
import { useReaderSetChaptersState } from '@/features/reader/hooks/useReaderSetChaptersState.ts';
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
import { useChapterListOptions } from '@/features/chapter/utils/ChapterList.util.tsx';
import { FALLBACK_MANGA } from '@/features/manga/Manga.constants.ts';
import { useReaderStore, useReaderStoreShallow } from '@/features/reader/ReaderStore.ts';
import { ReaderAutoScroll } from '@/features/reader/auto-scroll/ReaderAutoScroll.tsx';
2024-12-03 20:24:55 +01:00
2024-12-21 03:46:55 +01:00
const BaseReader = ({
setOverride,
readerNavBarWidth,
shouldSkipDupChapters,
shouldSkipFilteredChapters,
2024-12-21 03:46:55 +01:00
backgroundColor,
readingMode,
tapZoneLayout,
tapZoneInvertMode,
shouldShowReadingModePreview,
shouldShowTapZoneLayoutPreview,
2024-12-21 03:46:55 +01:00
setSettings,
mangaChapters,
2025-01-28 02:16:00 +01:00
initialChapter,
chapterForDuplicatesHandling,
2024-12-21 03:46:55 +01:00
currentChapter,
setReaderStateChapters,
setTotalPages,
setCurrentPageIndex,
setPageToScrollToIndex,
setPages,
setPageUrls,
setPageLoadStates,
setTransitionPageMode,
setShowPreview,
2025-05-03 13:00:03 +02:00
}: Pick<NavbarContextType, 'setOverride' | 'readerNavBarWidth'> &
2024-12-21 03:46:55 +01:00
Pick<TReaderStateSettingsContext, 'setSettings'> &
Pick<
IReaderSettings,
| 'shouldSkipDupChapters'
| 'shouldSkipFilteredChapters'
| 'backgroundColor'
| 'shouldShowReadingModePreview'
| 'shouldShowTapZoneLayoutPreview'
> &
Pick<IReaderSettingsWithDefaultFlag, 'readingMode' | 'tapZoneLayout' | 'tapZoneInvertMode'> &
2025-01-28 02:16:00 +01:00
Pick<
ReaderStateChapters,
| 'mangaChapters'
| 'initialChapter'
| 'chapterForDuplicatesHandling'
| 'currentChapter'
| 'setReaderStateChapters'
2025-01-28 02:16:00 +01:00
> &
2024-12-21 03:46:55 +01:00
Pick<
ReaderStatePages,
| 'setTotalPages'
| 'setCurrentPageIndex'
| 'setPageToScrollToIndex'
| 'setPages'
| 'setPageUrls'
| 'setPageLoadStates'
| 'setTransitionPageMode'
> &
Pick<TReaderTapZoneContext, 'setShowPreview'>) => {
2024-12-03 20:24:55 +01:00
const { t } = useTranslation();
const manga = useReaderStoreShallow((state) => state.manga);
const overlay = useReaderStoreShallow((state) => state.overlay);
2024-12-03 20:24:55 +01:00
const scrollElementRef = useRef<HTMLDivElement | null>(null);
const [areSettingsSet, setAreSettingsSet] = useState(false);
const { chapterSourceOrder: paramChapterSourceOrder, mangaId: paramMangaId } = useParams<{
chapterSourceOrder: string;
2024-12-03 20:24:55 +01:00
mangaId: string;
}>();
const chapterSourceOrder = Number(paramChapterSourceOrder);
2024-12-03 20:24:55 +01:00
const mangaId = Number(paramMangaId);
const mangaResponse = requestManager.useGetManga<GetMangaReaderQuery>(GET_MANGA_READER, mangaId);
const chaptersResponse = requestManager.useGetMangaChapters<GetChaptersReaderQuery>(GET_CHAPTERS_READER, mangaId);
2025-05-03 13:00:03 +02:00
useAppTitle(
!manga || !currentChapter
? t('reader.title', { mangaId, chapterIndex: chapterSourceOrder })
: `${manga.title}: ${currentChapter.name}`,
);
2024-12-03 20:24:55 +01:00
const {
metadata: defaultSettingsMetadata,
settings: defaultSettings,
request: defaultSettingsResponse,
} = useDefaultReaderSettings();
const chapterListOptions = useChapterListOptions(manga ?? FALLBACK_MANGA);
2024-12-03 20:24:55 +01:00
const isLoading =
currentChapter === undefined ||
!areSettingsSet ||
2024-12-03 20:24:55 +01:00
mangaResponse.loading ||
chaptersResponse.loading ||
2025-01-28 02:16:00 +01:00
defaultSettingsResponse.loading;
const error = mangaResponse.error ?? chaptersResponse.error ?? defaultSettingsResponse.error;
2024-12-03 20:24:55 +01:00
useEffect(() => {
useReaderStore.getState().setManga(mangaResponse.data?.manga);
2024-12-03 20:24:55 +01:00
}, [mangaResponse.data?.manga]);
2025-01-18 22:41:54 +01:00
useReaderResetStates(
setReaderStateChapters,
setCurrentPageIndex,
setPageToScrollToIndex,
setTotalPages,
setPages,
setPageUrls,
setPageLoadStates,
setTransitionPageMode,
setSettings,
);
2025-01-18 22:41:54 +01:00
useReaderSetSettingsState(
mangaResponse,
defaultSettingsResponse,
defaultSettings,
defaultSettingsMetadata,
setSettings,
setAreSettingsSet,
);
useReaderShowSettingPreviewOnChange(
isLoading,
error,
areSettingsSet,
2025-01-18 22:41:54 +01:00
readingMode,
tapZoneLayout,
tapZoneInvertMode,
shouldShowReadingModePreview,
shouldShowTapZoneLayoutPreview,
setShowPreview,
);
useReaderSetChaptersState(
chaptersResponse,
chapterSourceOrder,
mangaChapters,
initialChapter,
chapterForDuplicatesHandling,
2025-01-18 22:41:54 +01:00
setReaderStateChapters,
shouldSkipDupChapters,
shouldSkipFilteredChapters,
chapterListOptions,
2025-01-18 22:41:54 +01:00
);
2024-12-03 20:24:55 +01:00
useLayoutEffect(() => {
setOverride({
status: true,
value: (
<Box sx={{ position: 'absolute' }}>
<ReaderHotkeys scrollElementRef={scrollElementRef} />
<ReaderOverlay isVisible={overlay.isVisible} />
2024-12-03 20:24:55 +01:00
{!scrollElementRef.current && (
<Box
onClick={() => overlay.setIsVisible(!overlay.isVisible)}
2024-12-03 20:24:55 +01:00
sx={{
position: 'fixed',
top: 0,
left: 0,
2024-12-03 20:24:55 +01:00
width: '100vw',
height: '100vh',
background: 'transparent',
}}
/>
)}
</Box>
),
});
return () => setOverride({ status: false, value: null });
}, [overlay.isVisible, scrollElementRef.current]);
2024-12-03 20:24:55 +01:00
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
2024-12-21 23:27:03 +01:00
messageExtra={getErrorMessage(error)}
2024-12-03 20:24:55 +01:00
retry={() => {
if (mangaResponse.error) {
mangaResponse.refetch().catch(defaultPromiseErrorHandler('Reader::refetchManga'));
}
if (defaultSettingsResponse.error) {
defaultSettingsResponse
.refetch()
.catch(defaultPromiseErrorHandler('Reader::refetchDefaultSettings'));
}
if (chaptersResponse.error) {
chaptersResponse.refetch().catch(defaultPromiseErrorHandler('Reader::refetchChapters'));
}
}}
/>
);
}
if (isLoading) {
return (
<Box
sx={{
height: '100vh',
width: '100vw',
display: 'grid',
placeItems: 'center',
}}
>
<LoadingPlaceholder />
</Box>
);
}
2024-12-03 20:24:55 +01:00
if (currentChapter === null) {
return <EmptyViewAbsoluteCentered message={t('reader.error.label.chapter_not_found')} />;
}
if (!manga || !currentChapter) {
return null;
}
return (
<Box
sx={{
position: 'relative',
display: 'flex',
flexDirection: 'column',
width: `calc(100vw - ${readerNavBarWidth}px)`,
height: `100vh`,
marginLeft: `${readerNavBarWidth}px`,
transition: (theme) =>
`width 0.${theme.transitions.duration.shortest}s, margin-left 0.${theme.transitions.duration.shortest}s`,
overflow: 'auto',
backgroundColor: READER_BACKGROUND_TO_COLOR[backgroundColor],
}}
>
<ReaderViewer ref={scrollElementRef} />
<TapZoneLayout />
<ReaderRGBAFilter />
<ReaderAutoScroll />
</Box>
2024-12-03 20:24:55 +01:00
);
};
2024-12-21 03:46:55 +01:00
export const Reader = withPropsFrom(
memo(BaseReader),
[
useNavBarContext,
useReaderStateChaptersContext,
useReaderStateSettingsContext,
() => {
const {
shouldSkipDupChapters,
shouldSkipFilteredChapters,
backgroundColor,
shouldShowReadingModePreview,
shouldShowTapZoneLayoutPreview,
} = ReaderService.useSettingsWithoutDefaultFlag();
return {
shouldSkipDupChapters,
shouldSkipFilteredChapters,
backgroundColor,
shouldShowReadingModePreview,
shouldShowTapZoneLayoutPreview,
};
},
() => {
const { readingMode, tapZoneLayout, tapZoneInvertMode } = ReaderService.useSettings();
return { readingMode, tapZoneLayout, tapZoneInvertMode };
},
2024-12-21 03:46:55 +01:00
userReaderStatePagesContext,
useReaderTapZoneContext,
2024-12-21 03:46:55 +01:00
],
[
'setOverride',
'readerNavBarWidth',
'shouldSkipDupChapters',
'shouldSkipFilteredChapters',
'backgroundColor',
'readingMode',
'tapZoneLayout',
'tapZoneInvertMode',
'shouldShowReadingModePreview',
'shouldShowTapZoneLayoutPreview',
2024-12-21 03:46:55 +01:00
'setSettings',
'mangaChapters',
2025-01-28 02:16:00 +01:00
'initialChapter',
'chapterForDuplicatesHandling',
2024-12-21 03:46:55 +01:00
'currentChapter',
'setReaderStateChapters',
'setTotalPages',
'setCurrentPageIndex',
'setPageToScrollToIndex',
'setPages',
'setPageUrls',
'setPageLoadStates',
'setTransitionPageMode',
'setShowPreview',
2024-12-21 03:46:55 +01:00
],
);