/* * 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 CircularProgress from '@mui/material/CircularProgress'; import React, { useCallback, useContext, useEffect, useState } from 'react'; import { useHistory, useParams } from 'react-router-dom'; import HorizontalPager from 'components/reader/pager/HorizontalPager'; import PageNumber from 'components/reader/PageNumber'; import PagedPager from 'components/reader/pager/PagedPager'; import DoublePagedPager from 'components/reader/pager/DoublePagedPager'; import VerticalPager from 'components/reader/pager/VerticalPager'; import ReaderNavBar from 'components/navbar/ReaderNavBar'; import NavbarContext from 'components/context/NavbarContext'; import client from 'util/client'; import useLocalStorage from 'util/useLocalStorage'; import { Box } from '@mui/system'; import { requestUpdateMangaMetadata } from 'util/metadata'; import { checkAndHandleMissingStoredReaderSettings, getReaderSettingsFor, useDefaultReaderSettings, } from 'util/readerSettings'; import makeToast from 'components/util/Toast'; const getReaderComponent = (readerType: ReaderType) => { switch (readerType) { case 'ContinuesVertical': case 'Webtoon': return VerticalPager; break; case 'SingleVertical': case 'SingleRTL': case 'SingleLTR': return PagedPager; break; case 'DoubleVertical': case 'DoubleRTL': case 'DoubleLTR': return DoublePagedPager; break; case 'ContinuesHorizontalLTR': case 'ContinuesHorizontalRTL': return HorizontalPager; default: return VerticalPager; break; } }; const range = (n: number) => Array.from({ length: n }, (value, key) => key); const initialChapter = () => ({ pageCount: -1, index: -1, chapterCount: 0, lastPageRead: 0, name: 'Loading...', }); export default function Reader() { const history = useHistory(); const [serverAddress] = useLocalStorage('serverBaseURL', ''); const { chapterIndex, mangaId } = useParams<{ chapterIndex: string; mangaId: string }>(); const [manga, setManga] = useState({ id: +mangaId, title: '', thumbnailUrl: '', }); const [chapter, setChapter] = useState(initialChapter()); const [curPage, setCurPage] = useState(0); const [pageToScrollTo, setPageToScrollTo] = useState(undefined); const { setOverride, setTitle } = useContext(NavbarContext); const { settings: defaultSettings, loading: areDefaultSettingsLoading } = useDefaultReaderSettings(); const [settings, setSettings] = useState(getReaderSettingsFor(manga, defaultSettings)); const [isMangaLoading, setIsMangaLoading] = useState(true); const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => { setSettings({ ...settings, [key]: value }); requestUpdateMangaMetadata(manga, [[key, value]]).catch(() => makeToast('Failed to save the reader settings to the server', 'warning'), ); }; useEffect(() => { if (!manga?.title || (chapter as IChapter)?.name === 'Loading...') { setTitle(`Reader - Manga ${mangaId} Chapter ${chapterIndex}`); } else { setTitle(`${manga.title}: ${(chapter as IChapter).name}`); } }, [manga, chapter]); useEffect(() => { if (!areDefaultSettingsLoading && !isMangaLoading) { checkAndHandleMissingStoredReaderSettings(manga, 'manga', defaultSettings).catch(() => {}); setSettings(getReaderSettingsFor(manga, defaultSettings)); } }, [areDefaultSettingsLoading, isMangaLoading]); useEffect(() => { // set the custom navbar setOverride({ status: true, value: ( ), }); // clean up for when we leave the reader return () => setOverride({ status: false, value:
}); }, [manga, chapter, settings, curPage, chapterIndex]); useEffect(() => { setIsMangaLoading(true); client .get(`/api/v1/manga/${mangaId}/`) .then((response) => response.data) .then((data: IManga) => { setManga(data); setIsMangaLoading(false); }); }, [mangaId]); useEffect(() => { setChapter(initialChapter); client .get(`/api/v1/manga/${mangaId}/chapter/${chapterIndex}`) .then((response) => response.data) .then((data: IChapter) => { setChapter(data); if (data.lastPageRead === data.pageCount - 1) { // last page, also probably read = true, we will load the first page. setCurPage(0); } else setCurPage(data.lastPageRead); }); }, [chapterIndex]); useEffect(() => { if (curPage !== -1) { const formData = new FormData(); formData.append('lastPageRead', curPage.toString()); client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formData); } if (curPage === chapter.pageCount - 1) { const formDataRead = new FormData(); formDataRead.append('read', 'true'); client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formDataRead); } }, [curPage]); const nextChapter = useCallback(() => { if (chapter.index < chapter.chapterCount) { const formData = new FormData(); formData.append('lastPageRead', `${chapter.pageCount - 1}`); formData.append('read', 'true'); client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formData); history.replace({ pathname: `/manga/${manga.id}/chapter/${chapter.index + 1}`, state: history.location.state, }); } }, [chapter.index, chapter.chapterCount, chapter.pageCount, manga.id]); const prevChapter = useCallback(() => { if (chapter.index > 1) { history.replace({ pathname: `/manga/${manga.id}/chapter/${chapter.index - 1}`, state: history.location.state, }); } }, [chapter.index, manga.id]); // return spinner while chpater data is loading if (chapter.pageCount === -1) { return ( ); } const pages = range(chapter.pageCount).map((index) => ({ index, src: `${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterIndex}/page/${index}`, })); const ReaderComponent = getReaderComponent(settings.readerType); // last page, also probably read = true, we will load the first page. const initialPage = pageToScrollTo ?? (chapter.lastPageRead === chapter.pageCount - 1 ? 0 : chapter.lastPageRead); return ( ); }