Feature/batch chapter download (#191)

* Add NavbarToolbar component for rendering items in toolbar through portal

* Refactor Manga screen, move chapters filtering to toolbar menu, cleanup ChapterOptions

* Fix warning

* Update layout of ChapterList and add selection for chapters, add batch download, move stuff around

* Add space to bookmark icon

* Clear actions when leaving source mangas page

* Move bookmark icon back inline and align it correctly with text

* Fixup MangaDetails buttons to update cache properly so buttons in manga menu are updated

* Move chapters filter to ChapterList

* Fixup error display overlapping with loaded content when revalidation fails

* Fixup spacing around ChapterList title

* Only show small progress if manga is loaded so there is not to many spinners

* Prevent title wrapping

* Add icon buttons to desktop sizes of manga toolbar menu
This commit is contained in:
Valter Martinek
2022-11-09 18:09:15 +01:00
committed by GitHub
parent e828582fd4
commit 4a4e8b2cde
19 changed files with 988 additions and 552 deletions

View File

@@ -1,150 +0,0 @@
/*
* 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 React, {
useState, useEffect, useCallback, useMemo, useRef,
} from 'react';
import { Box, styled } from '@mui/system';
import { Virtuoso } from 'react-virtuoso';
import Typography from '@mui/material/Typography';
import { CircularProgress, Stack } from '@mui/material';
import makeToast from 'components/util/Toast';
import ChapterOptions from 'components/chapter/ChapterOptions';
import ChapterCard from 'components/chapter/ChapterCard';
import { useReducerLocalStorage } from 'util/useLocalStorage';
import {
chapterOptionsReducer, defaultChapterOptions, findFirstUnreadChapter,
filterAndSortChapters,
} from 'components/chapter/util';
import ResumeFab from 'components/chapter/ResumeFAB';
import useSubscription from 'components/library/useSubscription';
const CustomVirtuoso = styled(Virtuoso)(({ theme }) => ({
listStyle: 'none',
padding: 0,
minHeight: '200px',
[theme.breakpoints.up('md')]: {
width: '50vw',
// 64px for the Appbar, 48px for the ChapterCount Header
height: 'calc(100vh - 64px - 48px)',
margin: 0,
},
}));
interface IProps {
id: string
chaptersData: IChapter[] | undefined
onRefresh: () => void;
}
export default function ChapterList({ id, chaptersData, onRefresh }: IProps) {
const noChaptersFound = chaptersData?.length === 0;
const chapters = useMemo(() => chaptersData ?? [], [chaptersData]);
const [firstUnreadChapter, setFirstUnreadChapter] = useState<IChapter>();
const [filteredChapters, setFilteredChapters] = useState<IChapter[]>([]);
// eslint-disable-next-line max-len
const [options, optionsDispatch] = useReducerLocalStorage<ChapterListOptions, ChapterOptionsReducerAction>(
chapterOptionsReducer, `${id}filterOptions`, defaultChapterOptions,
);
const prevQueueRef = useRef<IDownloadChapter[]>();
const queue = useSubscription<IQueue>('/api/v1/downloads').data?.queue;
const downloadStatusStringFor = useCallback((chapter: IChapter) => {
let rtn = '';
if (chapter.downloaded) {
rtn = ' • Downloaded';
}
queue?.forEach((q) => {
if (chapter.index === q.chapterIndex && chapter.mangaId === q.mangaId) {
rtn = ` • Downloading (${(q.progress * 100).toFixed(2)}%)`;
}
});
return rtn;
}, [queue]);
useEffect(() => {
if (prevQueueRef.current && queue) {
const prevQueue = prevQueueRef.current;
const changedDownloads = queue.filter((cd) => {
const prevChapterDownload = prevQueue
.find((pcd) => cd.chapterIndex === pcd.chapterIndex
&& cd.mangaId === pcd.mangaId);
if (!prevChapterDownload) return true;
return cd.state !== prevChapterDownload.state;
});
if (changedDownloads.length > 0) {
onRefresh();
}
}
prevQueueRef.current = queue;
}, [queue]);
useEffect(() => {
const filtered = filterAndSortChapters(chapters, options);
setFilteredChapters(filtered);
setFirstUnreadChapter(findFirstUnreadChapter(filtered));
}, [options, chapters]);
useEffect(() => {
if (noChaptersFound) {
makeToast('No chapters found', 'warning');
}
}, [noChaptersFound]);
if (chapters.length === 0 || noChaptersFound) {
return (
<div style={{
margin: '10px auto',
display: 'flex',
justifyContent: 'center',
}}
>
<CircularProgress thickness={5} />
</div>
);
}
return (
<>
<Stack direction="column">
<Box sx={{
display: 'flex', justifyContent: 'space-between', px: 1.5, mt: 1,
}}
>
<Typography variant="h5">
{`${filteredChapters.length} Chapters`}
</Typography>
<ChapterOptions options={options} optionsDispatch={optionsDispatch} />
</Box>
<CustomVirtuoso
style={{ // override Virtuoso default values and set them with class
height: 'undefined',
// 900 is the md breakpoint in MUI
overflowY: window.innerWidth < 900 ? 'visible' : 'auto',
}}
totalCount={filteredChapters.length}
itemContent={(index:number) => (
<ChapterCard
showChapterNumber={options.showChapterNumber}
chapter={filteredChapters[index]}
downloadStatusString={downloadStatusStringFor(filteredChapters[index])}
triggerChaptersUpdate={onRefresh}
/>
)}
useWindowScroll={window.innerWidth < 900}
overscan={window.innerHeight * 0.5}
/>
</Stack>
{firstUnreadChapter && <ResumeFab chapter={firstUnreadChapter} mangaId={id} />}
</>
);
}