/* * 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'; import { memo, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { useParams } from 'react-router-dom'; import { useLingui } from '@lingui/react/macro'; import { useDefaultReaderSettings } from '@/features/reader/settings/ReaderSettingsMetadata.ts'; import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx'; import { ReaderOverlay } from '@/features/reader/overlay/ReaderOverlay.tsx'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import type { GetChaptersReaderQuery, GetMangaReaderQuery } from '@/lib/graphql/generated/graphql.ts'; import { GET_MANGA_READER } from '@/lib/graphql/manga/MangaQuery.ts'; import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx'; import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { GET_CHAPTERS_READER } from '@/lib/graphql/chapter/ChapterQuery.ts'; 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 { ReaderHotkeys } from '@/features/reader/hotkeys/ReaderHotkeys.tsx'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import type { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts'; import { withPropsFrom } from '@/base/hoc/withPropsFrom.tsx'; 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 { getReaderOverlayStore, getReaderStore, useReaderChaptersStore, useReaderPagesStore, useReaderSettingsStore, useReaderStore, } from '@/features/reader/stores/ReaderStore.ts'; import { ReaderAutoScroll } from '@/features/reader/auto-scroll/ReaderAutoScroll.tsx'; import { getReaderBackgroundColor, isContinuousReadingMode } from '@/features/reader/settings/ReaderSettings.utils.tsx'; import { getPage } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.utils.tsx'; import { getOptionForDirection } from '@/features/theme/services/ThemeCreator.ts'; import { READING_DIRECTION_TO_THEME_DIRECTION } from '@/features/reader/settings/ReaderSettings.constants.tsx'; import { useTheme } from '@mui/material/styles'; const BaseReader = ({ setOverride, readerNavBarWidth, }: Pick) => { const { t } = useLingui(); const theme = useTheme(); const manga = useReaderStore('manga'); const { mangaChapters, initialChapter, chapterForDuplicatesHandling, currentChapter } = useReaderChaptersStore( 'mangaChapters', 'initialChapter', 'chapterForDuplicatesHandling', 'currentChapter', ); const { shouldSkipDupChapters, shouldSkipFilteredChapters, backgroundColor, shouldShowReadingModePreview, shouldShowTapZoneLayoutPreview, useAutoBackgroundColorContinuousMode, } = useReaderSettingsStore( 'shouldSkipDupChapters', 'shouldSkipFilteredChapters', 'backgroundColor', 'shouldShowReadingModePreview', 'shouldShowTapZoneLayoutPreview', 'useAutoBackgroundColorContinuousMode', ); const readingMode = useReaderSettingsStore('readingMode'); const readingDirection = useReaderSettingsStore('readingDirection'); const tapZoneLayout = useReaderSettingsStore('tapZoneLayout'); const tapZoneInvertMode = useReaderSettingsStore('tapZoneInvertMode'); const safeAreaInset = useReaderSettingsStore('safeAreaInset'); const { invertColors, applySepia, applyGrayscale } = useReaderSettingsStore((state) => ({ invertColors: state.customFilter.invert, applySepia: state.customFilter.sepia, applyGrayscale: state.customFilter.grayscale, })); const currentPageIndex = useReaderPagesStore('currentPageIndex'); const pages = useReaderPagesStore('pages'); const pageBackgroundColors = useReaderPagesStore('pageBackgroundColors'); const scrollElementRef = useRef(null); const [areSettingsSet, setAreSettingsSet] = useState(false); const { chapterSourceOrder: paramChapterSourceOrder, mangaId: paramMangaId } = useParams<{ chapterSourceOrder: string; mangaId: string; }>(); const chapterSourceOrder = Number(paramChapterSourceOrder); const mangaId = Number(paramMangaId); const mangaResponse = requestManager.useGetManga(GET_MANGA_READER, mangaId); const chaptersResponse = requestManager.useGetMangaChapters(GET_CHAPTERS_READER, mangaId); useAppTitle( !manga || !currentChapter ? t`Reader — Manga ${mangaId} Chapter ${chapterSourceOrder}` : `${manga.title}: ${currentChapter.name}`, ); const { metadata: defaultSettingsMetadata, settings: defaultSettings, request: defaultSettingsResponse, } = useDefaultReaderSettings(); const chapterListOptions = useChapterListOptions(manga ?? FALLBACK_MANGA); const isLoading = currentChapter === undefined || !areSettingsSet || mangaResponse.loading || (chaptersResponse.loading && chaptersResponse.dataState !== 'complete') || defaultSettingsResponse.loading; const error = mangaResponse.error ?? chaptersResponse.error ?? defaultSettingsResponse.error; const page = getPage(currentPageIndex, pages); const primaryPageBackground = getReaderBackgroundColor( backgroundColor, pageBackgroundColors[page.primary.index]?.color, isContinuousReadingMode(readingMode.value), useAutoBackgroundColorContinuousMode, theme, invertColors, applySepia, applyGrayscale, ); const secondaryPageBackground = getReaderBackgroundColor( backgroundColor, pageBackgroundColors[page.secondary?.index ?? page.primary.index]?.color, isContinuousReadingMode(readingMode.value), useAutoBackgroundColorContinuousMode, theme, invertColors, applySepia, applyGrayscale, ); const direction = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection.value]; useEffect(() => { getReaderStore().setManga(mangaResponse.data?.manga); }, [mangaResponse.data?.manga]); useReaderResetStates(); useReaderSetSettingsState( mangaResponse, defaultSettingsResponse, defaultSettings, defaultSettingsMetadata, setAreSettingsSet, ); useReaderShowSettingPreviewOnChange( isLoading, error, areSettingsSet, readingMode, tapZoneLayout, tapZoneInvertMode, shouldShowReadingModePreview, shouldShowTapZoneLayoutPreview, ); useReaderSetChaptersState( chaptersResponse, chapterSourceOrder, mangaChapters, initialChapter, chapterForDuplicatesHandling, shouldSkipDupChapters, shouldSkipFilteredChapters, chapterListOptions, ); useLayoutEffect(() => { setOverride({ status: true, value: ( {!scrollElementRef.current && ( getReaderOverlayStore().setIsVisible(!getReaderOverlayStore().isVisible)} sx={{ position: 'fixed', top: 0, left: 0, width: '100vw', height: '100vh', background: 'transparent', }} /> )} ), }); return () => setOverride({ status: false, value: null }); }, [scrollElementRef.current]); if (error) { return ( { 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 ( ); } if (currentChapter === null) { return ; } if (!manga || !currentChapter) { return null; } return ( ); }; export const Reader = withPropsFrom(memo(BaseReader), [useNavBarContext], ['setOverride', 'readerNavBarWidth']);