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';
|
2025-02-20 13:16:39 +01:00
|
|
|
import { useParams } from 'react-router-dom';
|
2026-01-10 19:45:02 +01:00
|
|
|
import { useLingui } from '@lingui/react/macro';
|
2025-08-16 02:02:13 +02:00
|
|
|
import { useDefaultReaderSettings } from '@/features/reader/settings/ReaderSettingsMetadata.ts';
|
2025-08-16 02:14:49 +02:00
|
|
|
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';
|
2026-03-11 00:11:29 +01:00
|
|
|
import type { GetChaptersReaderQuery, GetMangaReaderQuery } from '@/lib/graphql/generated/graphql.ts';
|
2025-12-22 14:00:44 +01:00
|
|
|
import { GET_MANGA_READER } from '@/lib/graphql/manga/MangaQuery.ts';
|
2025-08-16 02:48:46 +02:00
|
|
|
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-12-22 14:00:44 +01:00
|
|
|
import { GET_CHAPTERS_READER } from '@/lib/graphql/chapter/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';
|
|
|
|
|
import { ReaderViewer } from '@/features/reader/viewer/ReaderViewer.tsx';
|
|
|
|
|
import { READER_BACKGROUND_TO_COLOR } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
|
|
|
|
import { ReaderHotkeys } from '@/features/reader/hotkeys/ReaderHotkeys.tsx';
|
2024-12-21 23:27:03 +01:00
|
|
|
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
2026-03-11 00:11:29 +01:00
|
|
|
import type { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
2025-08-16 02:48:46 +02:00
|
|
|
import { withPropsFrom } from '@/base/hoc/withPropsFrom.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';
|
2025-09-21 00:50:19 +02:00
|
|
|
import {
|
2025-09-27 15:36:50 +02:00
|
|
|
getReaderOverlayStore,
|
2025-09-21 00:50:19 +02:00
|
|
|
getReaderStore,
|
|
|
|
|
useReaderChaptersStore,
|
|
|
|
|
useReaderSettingsStore,
|
|
|
|
|
useReaderStore,
|
|
|
|
|
} from '@/features/reader/stores/ReaderStore.ts';
|
2025-08-31 22:39:30 +02:00
|
|
|
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,
|
2025-09-20 14:41:05 +02:00
|
|
|
}: Pick<NavbarContextType, 'setOverride' | 'readerNavBarWidth'>) => {
|
2026-01-10 19:45:02 +01:00
|
|
|
const { t } = useLingui();
|
2026-03-11 22:10:34 +01:00
|
|
|
const manga = useReaderStore('manga');
|
2025-09-21 00:50:19 +02:00
|
|
|
const { mangaChapters, initialChapter, chapterForDuplicatesHandling, currentChapter } = useReaderChaptersStore(
|
2026-03-11 22:10:34 +01:00
|
|
|
'mangaChapters',
|
|
|
|
|
'initialChapter',
|
|
|
|
|
'chapterForDuplicatesHandling',
|
|
|
|
|
'currentChapter',
|
2025-09-21 00:50:19 +02:00
|
|
|
);
|
2025-09-20 14:15:40 +02:00
|
|
|
const {
|
|
|
|
|
shouldSkipDupChapters,
|
|
|
|
|
shouldSkipFilteredChapters,
|
|
|
|
|
backgroundColor,
|
|
|
|
|
readingMode,
|
|
|
|
|
tapZoneLayout,
|
|
|
|
|
tapZoneInvertMode,
|
|
|
|
|
shouldShowReadingModePreview,
|
|
|
|
|
shouldShowTapZoneLayoutPreview,
|
2026-03-11 22:10:34 +01:00
|
|
|
} = useReaderSettingsStore(
|
|
|
|
|
'shouldSkipDupChapters',
|
|
|
|
|
'shouldSkipFilteredChapters',
|
|
|
|
|
'backgroundColor',
|
|
|
|
|
'readingMode',
|
|
|
|
|
'tapZoneLayout',
|
|
|
|
|
'tapZoneInvertMode',
|
|
|
|
|
'shouldShowReadingModePreview',
|
|
|
|
|
'shouldShowTapZoneLayoutPreview',
|
|
|
|
|
);
|
2024-12-03 20:24:55 +01:00
|
|
|
|
|
|
|
|
const scrollElementRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
|
|
2024-12-31 02:29:07 +01:00
|
|
|
const [areSettingsSet, setAreSettingsSet] = useState(false);
|
|
|
|
|
|
2024-12-31 13:54:16 +01:00
|
|
|
const { chapterSourceOrder: paramChapterSourceOrder, mangaId: paramMangaId } = useParams<{
|
|
|
|
|
chapterSourceOrder: string;
|
2024-12-03 20:24:55 +01:00
|
|
|
mangaId: string;
|
|
|
|
|
}>();
|
2024-12-31 13:54:16 +01:00
|
|
|
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
|
2026-01-10 19:45:02 +01:00
|
|
|
? t`Reader — Manga ${mangaId} Chapter ${chapterSourceOrder}`
|
2025-05-03 13:00:03 +02:00
|
|
|
: `${manga.title}: ${currentChapter.name}`,
|
|
|
|
|
);
|
|
|
|
|
|
2024-12-03 20:24:55 +01:00
|
|
|
const {
|
|
|
|
|
metadata: defaultSettingsMetadata,
|
|
|
|
|
settings: defaultSettings,
|
|
|
|
|
request: defaultSettingsResponse,
|
|
|
|
|
} = useDefaultReaderSettings();
|
2025-07-01 00:37:29 +02:00
|
|
|
const chapterListOptions = useChapterListOptions(manga ?? FALLBACK_MANGA);
|
2024-12-03 20:24:55 +01:00
|
|
|
|
|
|
|
|
const isLoading =
|
2024-12-31 13:59:34 +01:00
|
|
|
currentChapter === undefined ||
|
2024-12-31 02:29:07 +01:00
|
|
|
!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(() => {
|
2025-09-21 00:28:38 +02:00
|
|
|
getReaderStore().setManga(mangaResponse.data?.manga);
|
2024-12-03 20:24:55 +01:00
|
|
|
}, [mangaResponse.data?.manga]);
|
|
|
|
|
|
2025-09-20 14:15:40 +02:00
|
|
|
useReaderResetStates();
|
2025-01-18 22:41:54 +01:00
|
|
|
useReaderSetSettingsState(
|
|
|
|
|
mangaResponse,
|
|
|
|
|
defaultSettingsResponse,
|
|
|
|
|
defaultSettings,
|
|
|
|
|
defaultSettingsMetadata,
|
|
|
|
|
setAreSettingsSet,
|
|
|
|
|
);
|
|
|
|
|
useReaderShowSettingPreviewOnChange(
|
2024-12-30 21:23:39 +01:00
|
|
|
isLoading,
|
|
|
|
|
error,
|
2024-12-31 02:26:01 +01:00
|
|
|
areSettingsSet,
|
2025-01-18 22:41:54 +01:00
|
|
|
readingMode,
|
|
|
|
|
tapZoneLayout,
|
|
|
|
|
tapZoneInvertMode,
|
|
|
|
|
shouldShowReadingModePreview,
|
|
|
|
|
shouldShowTapZoneLayoutPreview,
|
|
|
|
|
);
|
|
|
|
|
useReaderSetChaptersState(
|
|
|
|
|
chaptersResponse,
|
|
|
|
|
chapterSourceOrder,
|
2025-07-01 00:37:29 +02:00
|
|
|
mangaChapters,
|
2025-02-20 13:16:39 +01:00
|
|
|
initialChapter,
|
2025-02-01 20:03:02 +01:00
|
|
|
chapterForDuplicatesHandling,
|
2025-01-18 22:41:54 +01:00
|
|
|
shouldSkipDupChapters,
|
2025-07-01 00:37:29 +02:00
|
|
|
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' }}>
|
2024-12-24 17:21:25 +01:00
|
|
|
<ReaderHotkeys scrollElementRef={scrollElementRef} />
|
2025-09-27 15:36:50 +02:00
|
|
|
<ReaderOverlay />
|
2024-12-03 20:24:55 +01:00
|
|
|
{!scrollElementRef.current && (
|
|
|
|
|
<Box
|
2025-09-27 15:36:50 +02:00
|
|
|
onClick={() => getReaderOverlayStore().setIsVisible(!getReaderOverlayStore().isVisible)}
|
2024-12-03 20:24:55 +01:00
|
|
|
sx={{
|
2024-12-24 17:25:12 +01:00
|
|
|
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 });
|
2025-09-27 15:36:50 +02:00
|
|
|
}, [scrollElementRef.current]);
|
2024-12-03 20:24:55 +01:00
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
return (
|
|
|
|
|
<EmptyViewAbsoluteCentered
|
2026-01-10 19:45:02 +01:00
|
|
|
message={t`Unable 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'));
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-31 02:29:07 +01:00
|
|
|
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) {
|
2026-01-10 19:45:02 +01:00
|
|
|
return <EmptyViewAbsoluteCentered message={t`Chapter does not exist`} />;
|
2024-12-03 20:24:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!manga || !currentChapter) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2024-12-24 17:21:25 +01:00
|
|
|
<Box
|
|
|
|
|
sx={{
|
|
|
|
|
position: 'relative',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
flexDirection: 'column',
|
2025-09-27 15:50:26 +02:00
|
|
|
minWidth: `calc(100vw - ${readerNavBarWidth}px)`,
|
|
|
|
|
maxWidth: `calc(100vw - ${readerNavBarWidth}px)`,
|
2024-12-24 17:21:25 +01:00
|
|
|
width: `calc(100vw - ${readerNavBarWidth}px)`,
|
|
|
|
|
height: `100vh`,
|
2026-05-05 13:42:10 +02:00
|
|
|
pt: 'env(safe-area-inset-top)',
|
|
|
|
|
pb: 'env(safe-area-inset-bottom)',
|
|
|
|
|
pr: 'env(safe-area-inset-right)',
|
|
|
|
|
pl: 'env(safe-area-inset-left)',
|
2024-12-24 17:21:25 +01:00
|
|
|
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 />
|
2025-08-31 22:39:30 +02:00
|
|
|
<ReaderAutoScroll />
|
2024-12-24 17:21:25 +01:00
|
|
|
</Box>
|
2024-12-03 20:24:55 +01:00
|
|
|
);
|
|
|
|
|
};
|
2024-12-21 03:46:55 +01:00
|
|
|
|
2025-09-20 14:15:40 +02:00
|
|
|
export const Reader = withPropsFrom(memo(BaseReader), [useNavBarContext], ['setOverride', 'readerNavBarWidth']);
|