Due to horizontally centering the flex-box items via "alignItems", the items did not stretch anymore, which is the default value for "alignItems" and thus, did not take up the available width.
Regression introduced with ab0ecf4da0
444 lines
16 KiB
TypeScript
444 lines
16 KiB
TypeScript
/*
|
|
* 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 { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
|
import { Box } from '@mui/material';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { AllowedMetadataValueTypes, ChapterOffset, IReaderSettings, ReaderType, TChapter, TManga } from '@/typings';
|
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
|
import {
|
|
checkAndHandleMissingStoredReaderSettings,
|
|
getReaderSettingsFor,
|
|
useDefaultReaderSettings,
|
|
} from '@/util/readerSettings';
|
|
import { requestUpdateMangaMetadata } from '@/util/metadata';
|
|
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 { makeToast } from '@/components/util/Toast';
|
|
import { NavBarContext } from '@/components/context/NavbarContext.tsx';
|
|
import { useDebounce } from '@/util/useDebounce.ts';
|
|
import { UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
|
|
import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';
|
|
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
|
import { Chapters } from '@/lib/data/Chapters.ts';
|
|
|
|
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,
|
|
sourceOrder: -1,
|
|
chapterCount: 0,
|
|
lastPageRead: 0,
|
|
name: 'Loading...',
|
|
} as unknown as TChapter;
|
|
|
|
export function Reader() {
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
|
|
const { chapterIndex, mangaId } = useParams<{ chapterIndex: string; mangaId: string }>();
|
|
|
|
const initialManga = useMemo(
|
|
() =>
|
|
({
|
|
id: +mangaId,
|
|
title: '',
|
|
thumbnailUrl: '',
|
|
genre: [],
|
|
inLibraryAt: 0,
|
|
lastReadAt: 0,
|
|
chapters: { totalCount: 0 },
|
|
}) as unknown as TManga,
|
|
[mangaId],
|
|
);
|
|
|
|
const { data, loading: isMangaLoading } = requestManager.useGetManga(mangaId);
|
|
const loadedChapter = useRef<TChapter | null>(null);
|
|
const isChapterLoaded =
|
|
Number(mangaId) === loadedChapter.current?.manga.id &&
|
|
Number(chapterIndex) === loadedChapter.current?.sourceOrder &&
|
|
loadedChapter.current?.pageCount !== -1;
|
|
const manga = data?.manga ?? initialManga;
|
|
const { data: chapterData, loading: isChapterLoading } = requestManager.useGetMangaChapter(mangaId, chapterIndex);
|
|
const arePagesUpdatedRef = useRef(false);
|
|
|
|
const {
|
|
settings: { downloadAheadLimit },
|
|
} = useMetadataServerSettings();
|
|
const isDownloadAheadEnabled = !!downloadAheadLimit;
|
|
|
|
const getLoadedChapter = () => {
|
|
const isAChapterLoaded = loadedChapter.current;
|
|
|
|
const isSameAsLoadedChapter = isAChapterLoaded && isChapterLoaded;
|
|
if (isSameAsLoadedChapter) {
|
|
const didPageCountChange =
|
|
chapterData?.chapter && loadedChapter.current?.pageCount !== chapterData.chapter.pageCount;
|
|
return didPageCountChange ? chapterData!.chapter : loadedChapter.current;
|
|
}
|
|
|
|
arePagesUpdatedRef.current = false;
|
|
|
|
if (chapterData?.chapter) {
|
|
return chapterData.chapter;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
loadedChapter.current = getLoadedChapter();
|
|
|
|
const chapter = loadedChapter.current ?? initialChapter;
|
|
const [fetchPages] = requestManager.useGetChapterPagesFetch(chapter.id);
|
|
|
|
useEffect(() => {
|
|
const shouldFetchPages = !isChapterLoading && !chapter.isDownloaded;
|
|
if (shouldFetchPages) {
|
|
fetchPages().then(() => {
|
|
arePagesUpdatedRef.current = true;
|
|
});
|
|
} else {
|
|
arePagesUpdatedRef.current = true;
|
|
}
|
|
}, [chapter.id]);
|
|
|
|
const isLoading = isChapterLoading || !arePagesUpdatedRef.current;
|
|
const [wasLastPageReadSet, setWasLastPageReadSet] = useState(false);
|
|
const [curPage, setCurPage] = useState<number>(0);
|
|
const isLastPage = curPage === chapter.pageCount - 1;
|
|
const curPageDebounced = useDebounce(curPage, isLastPage ? 0 : 1000);
|
|
const [pageToScrollTo, setPageToScrollTo] = useState<number | undefined>(undefined);
|
|
const { setOverride, setTitle } = useContext(NavBarContext);
|
|
const [retrievingNextChapter, setRetrievingNextChapter] = useState(false);
|
|
const { data: mangaChaptersData } = requestManager.useGetMangaChapters(mangaId, { nextFetchPolicy: 'standby' });
|
|
const mangaChapters = mangaChaptersData?.chapters.nodes;
|
|
|
|
const { settings: defaultSettings, loading: areDefaultSettingsLoading } = useDefaultReaderSettings();
|
|
const [settings, setSettings] = useState(getReaderSettingsFor(manga, defaultSettings));
|
|
|
|
const { settings: metadataSettings } = useMetadataServerSettings();
|
|
|
|
const prevChapters = useMemo(
|
|
() =>
|
|
Chapters.getNextChapters(chapter, mangaChapters ?? [], {
|
|
offset: ChapterOffset.PREV,
|
|
skipDupe: settings.skipDupChapters,
|
|
}),
|
|
[chapter, mangaChapters, settings.skipDupChapters],
|
|
);
|
|
const nextChapters = useMemo(
|
|
() => Chapters.getNextChapters(chapter, mangaChapters ?? [], { skipDupe: settings.skipDupChapters }),
|
|
[chapter, mangaChapters, settings.skipDupChapters],
|
|
);
|
|
const prevChapter = useMemo(
|
|
() =>
|
|
Chapters.getNextChapter(chapter, mangaChapters ?? [], {
|
|
offset: ChapterOffset.PREV,
|
|
skipDupe: settings.skipDupChapters,
|
|
}),
|
|
[chapter, mangaChapters, settings.skipDupChapters],
|
|
);
|
|
const nextChapter = useMemo(
|
|
() =>
|
|
Chapters.getNextChapter(chapter, mangaChapters ?? [], {
|
|
skipDupe: settings.skipDupChapters,
|
|
}),
|
|
[chapter, mangaChapters, settings.skipDupChapters],
|
|
);
|
|
|
|
const updateChapter = (patch: UpdateChapterPatchInput) => {
|
|
if (chapter === initialChapter) {
|
|
return;
|
|
}
|
|
|
|
const getChapterIdToDelete = () => {
|
|
const isAutoDeletionEnabled = !!patch.isRead && !!metadataSettings.deleteChaptersWhileReading;
|
|
if (!isAutoDeletionEnabled || !mangaChapters) {
|
|
return -1;
|
|
}
|
|
|
|
const chapterToDelete = [chapter, ...prevChapters][metadataSettings.deleteChaptersWhileReading - 1];
|
|
|
|
if (!chapterToDelete) {
|
|
return -1;
|
|
}
|
|
|
|
// chapter has to exist in the cache since the reader fetches all chapters of the manga
|
|
const chapterToDeleteUpToDateData = Chapters.getFromCache<TChapter>(chapterToDelete.id)!;
|
|
|
|
const shouldDeleteChapter =
|
|
chapterToDeleteUpToDateData.isRead &&
|
|
Chapters.isAutoDeletable(chapterToDeleteUpToDateData, metadataSettings.deleteChaptersWithBookmark);
|
|
if (!shouldDeleteChapter) {
|
|
return -1;
|
|
}
|
|
|
|
return chapterToDelete.id;
|
|
};
|
|
|
|
const downloadAhead = () => {
|
|
const currentChapter = Chapters.getFromCache<TChapter>(chapter.id);
|
|
|
|
const inDownloadRange = (patch.lastPageRead ?? 0) / chapter.pageCount > 0.25;
|
|
const shouldCheckDownloadAhead =
|
|
isDownloadAheadEnabled && chapter.manga.inLibrary && !!currentChapter?.isDownloaded && inDownloadRange;
|
|
|
|
if (shouldCheckDownloadAhead) {
|
|
const nextChapterUpToDate = nextChapter ? Chapters.getFromCache<TChapter>(nextChapter.id) : null;
|
|
|
|
if (!nextChapterUpToDate?.isDownloaded) {
|
|
return;
|
|
}
|
|
|
|
const nextChaptersUpToDate = Chapters.getNonRead(nextChapters).map(
|
|
// the chapters have to be in the cache since the reader fetches the whole chapter list of the manga
|
|
(mangaChapter) => Chapters.getFromCache<TChapter>(mangaChapter.id)!,
|
|
);
|
|
|
|
const chapterIdsToDownload = nextChaptersUpToDate
|
|
// "settingsData" can't be undefined since this would not get executed otherwise
|
|
.slice(-downloadAheadLimit)
|
|
.filter((mangaChapter) => !mangaChapter.isDownloaded)
|
|
.map((mangaChapter) => mangaChapter.id)
|
|
.filter((id) => !Chapters.isDownloading(id));
|
|
|
|
if (!chapterIdsToDownload.length) {
|
|
return;
|
|
}
|
|
|
|
Chapters.download(chapterIdsToDownload!).catch(
|
|
defaultPromiseErrorHandler('Reader::updateChapter: shouldDownloadAhead'),
|
|
);
|
|
}
|
|
};
|
|
|
|
downloadAhead();
|
|
|
|
requestManager
|
|
.updateChapter(chapter.id, {
|
|
...patch,
|
|
chapterIdToDelete: getChapterIdToDelete(),
|
|
})
|
|
.response.catch();
|
|
};
|
|
|
|
const setSettingValue = (key: keyof IReaderSettings, value: AllowedMetadataValueTypes) => {
|
|
setSettings({ ...settings, [key]: value });
|
|
requestUpdateMangaMetadata(manga, [[key, value]]).catch(() =>
|
|
makeToast(t('reader.settings.error.label.failed_to_save_settings'), 'warning'),
|
|
);
|
|
};
|
|
|
|
const openNextChapter = useCallback(
|
|
(offset: ChapterOffset) => {
|
|
const isOpenNextChapter = offset === ChapterOffset.NEXT;
|
|
const chapterToOpen = isOpenNextChapter ? nextChapter : prevChapter;
|
|
|
|
if (!chapterToOpen) {
|
|
makeToast(
|
|
t(
|
|
isOpenNextChapter
|
|
? 'reader.error.label.next_chapter_does_not_exist'
|
|
: 'reader.error.label.prev_chapter_does_not_exist',
|
|
),
|
|
'error',
|
|
);
|
|
return;
|
|
}
|
|
|
|
setRetrievingNextChapter(true);
|
|
setCurPage(0);
|
|
|
|
navigate(`/manga/${manga.id}/chapter/${chapterToOpen.sourceOrder}`, {
|
|
replace: true,
|
|
state: location.state,
|
|
});
|
|
|
|
setRetrievingNextChapter(false);
|
|
},
|
|
[manga.id, prevChapter?.id, nextChapter?.id],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (isLoading || !chapter) {
|
|
setCurPage(0);
|
|
return;
|
|
}
|
|
|
|
setWasLastPageReadSet(true);
|
|
if (chapter.lastPageRead === chapter.pageCount - 1) {
|
|
// last page, also probably read = true, we will load the first page.
|
|
setCurPage(0);
|
|
} else setCurPage(chapter.lastPageRead);
|
|
}, [chapter, isLoading]);
|
|
|
|
useEffect(() => {
|
|
if (!manga?.title || chapter.name === t('global.label.loading')) {
|
|
setTitle(t('reader.title'));
|
|
} else {
|
|
setTitle(`${manga.title}: ${chapter.name}`);
|
|
}
|
|
}, [t, manga, chapter]);
|
|
|
|
useEffect(() => {
|
|
if (!areDefaultSettingsLoading && !isMangaLoading) {
|
|
checkAndHandleMissingStoredReaderSettings(manga, 'manga', defaultSettings).catch(
|
|
defaultPromiseErrorHandler('Reader::checkAndHandleMissingStoredReaderSettings'),
|
|
);
|
|
setSettings(getReaderSettingsFor(manga, defaultSettings));
|
|
}
|
|
}, [areDefaultSettingsLoading, isMangaLoading]);
|
|
|
|
useEffect(() => {
|
|
// set the custom navbar
|
|
setOverride({
|
|
status: true,
|
|
value: (
|
|
<ReaderNavBar
|
|
settings={settings}
|
|
setSettingValue={setSettingValue}
|
|
manga={manga}
|
|
chapter={chapter}
|
|
curPage={curPage}
|
|
scrollToPage={setPageToScrollTo}
|
|
openNextChapter={openNextChapter}
|
|
retrievingNextChapter={retrievingNextChapter}
|
|
/>
|
|
),
|
|
});
|
|
|
|
// clean up for when we leave the reader
|
|
return () => setOverride({ status: false, value: <div /> });
|
|
}, [manga, chapter, settings, curPage, chapterIndex, retrievingNextChapter]);
|
|
|
|
useEffect(() => {
|
|
if (!wasLastPageReadSet) {
|
|
return;
|
|
}
|
|
|
|
if (chapter === initialChapter) {
|
|
return;
|
|
}
|
|
|
|
// do not mutate the chapter, this will cause the page to jump around due to always scrolling to the last read page
|
|
const updateLastPageRead = curPageDebounced !== -1;
|
|
const updateIsRead = curPageDebounced === chapter.pageCount - 1;
|
|
const shouldUpdateChapter = updateLastPageRead || updateIsRead;
|
|
if (!shouldUpdateChapter) {
|
|
return;
|
|
}
|
|
|
|
updateChapter({
|
|
lastPageRead: updateLastPageRead ? curPageDebounced : undefined,
|
|
isRead: updateIsRead ? true : undefined,
|
|
});
|
|
}, [curPageDebounced, isDownloadAheadEnabled]);
|
|
|
|
const loadNextChapter = useCallback(() => {
|
|
updateChapter({
|
|
lastPageRead: chapter.pageCount - 1,
|
|
isRead: true,
|
|
});
|
|
|
|
openNextChapter(ChapterOffset.NEXT);
|
|
}, [chapter.pageCount, openNextChapter]);
|
|
|
|
const loadPrevChapter = useCallback(() => {
|
|
openNextChapter(ChapterOffset.PREV);
|
|
}, [openNextChapter]);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Box
|
|
sx={{
|
|
height: '100vh',
|
|
width: '100vw',
|
|
display: 'grid',
|
|
placeItems: 'center',
|
|
}}
|
|
>
|
|
<CircularProgress thickness={5} />
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
const pages = range(chapter.pageCount).map((index) => ({
|
|
index,
|
|
src: requestManager.getChapterPageUrl(mangaId, chapterIndex, 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 (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignContent: 'center',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
minWidth: settings.staticNav
|
|
? 'calc((100vw - (100vw - 100%)) - 300px)'
|
|
: 'calc(100vw - (100vw - 100%))', // 100vw = width excluding scrollbar; 100% = width including scrollbar
|
|
minHeight: '100vh',
|
|
marginLeft: settings.staticNav ? '300px' : 'unset',
|
|
}}
|
|
>
|
|
<PageNumber settings={settings} curPage={curPage} pageCount={chapter.pageCount} />
|
|
<Box sx={{ alignSelf: 'stretch' }}>
|
|
<ReaderComponent
|
|
key={chapter.id}
|
|
pages={pages}
|
|
pageCount={chapter.pageCount}
|
|
setCurPage={setCurPage}
|
|
initialPage={initialPage}
|
|
curPage={curPage}
|
|
settings={settings}
|
|
manga={manga}
|
|
chapter={chapter}
|
|
nextChapter={loadNextChapter}
|
|
prevChapter={loadPrevChapter}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|