/* * 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, CircularProgress, Stack, styled, Tooltip } from '@mui/material'; import Typography from '@mui/material/Typography'; import React, { useMemo } from 'react'; import { Virtuoso } from 'react-virtuoso'; import { useTranslation } from 'react-i18next'; import IconButton from '@mui/material/IconButton'; import DownloadIcon from '@mui/icons-material/Download'; import DoneAllIcon from '@mui/icons-material/DoneAll'; import { TChapter, TManga } from '@/typings.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { ChapterCard } from '@/components/chapter/ChapterCard.tsx'; import { ResumeFab } from '@/components/manga/ResumeFAB.tsx'; import { filterAndSortChapters, useChapterOptions } from '@/components/chapter/util.tsx'; import { EmptyView } from '@/components/util/EmptyView.tsx'; import { ChaptersToolbarMenu } from '@/components/chapter/ChaptersToolbarMenu.tsx'; import { SelectionFAB } from '@/components/collection/SelectionFAB.tsx'; import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab.tsx'; import { DownloadType } from '@/lib/graphql/generated/graphql.ts'; import { useSelectableCollection } from '@/components/collection/useSelectableCollection.ts'; import { SelectableCollectionSelectAll } from '@/components/collection/SelectableCollectionSelectAll.tsx'; import { Chapters } from '@/lib/data/Chapters.ts'; import { ChaptersWithMeta } from '@/lib/data/ChaptersWithMeta.ts'; import { ChapterActionMenuItems } from '@/components/chapter/ChapterActionMenuItems.tsx'; const ChapterListHeader = styled(Stack)(({ theme }) => ({ margin: 8, marginBottom: 0, marginRight: '10px', minHeight: 40, [theme.breakpoints.down('md')]: { marginRight: 0, }, })); const StyledVirtuoso = 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, }, })); export interface IChapterWithMeta { chapter: TChapter; downloadChapter: DownloadType | undefined; selected: boolean | null; } interface IProps { manga: TManga; isRefreshing: boolean; } export const ChapterList: React.FC = ({ manga, isRefreshing }) => { const { t } = useTranslation(); const { data: downloaderData } = requestManager.useGetDownloadStatus(); const queue = (downloaderData?.downloadStatus.queue as DownloadType[]) ?? []; const [options, dispatch] = useChapterOptions(manga.id); const { data: chaptersData, loading: isLoading } = requestManager.useGetMangaChapters(manga.id); const chapters = useMemo(() => chaptersData?.chapters.nodes ?? [], [chaptersData?.chapters.nodes]); const { areNoItemsSelected, areAllItemsSelected, selectedItemIds, handleSelectAll, handleSelection } = useSelectableCollection(chapters.length, { currentKey: 'default' }); const visibleChapters = useMemo(() => filterAndSortChapters(chapters, options), [chapters, options]); const nextChapterIndexToRead = (manga.latestReadChapter?.sourceOrder ?? 0) + 1; const isLatestChapterRead = manga.chapters.totalCount === manga.latestReadChapter?.sourceOrder; const areAllChaptersRead = manga.unreadCount === 0; const areAllChaptersDownloaded = manga.downloadCount === manga.chapters.totalCount; const noChaptersFound = chapters.length === 0; const noChaptersMatchingFilter = !noChaptersFound && visibleChapters.length === 0; const chaptersWithMeta: IChapterWithMeta[] = useMemo( () => visibleChapters.map((chapter) => { const downloadChapter = queue?.find( (cd) => cd.chapter.sourceOrder === chapter.sourceOrder && cd.chapter.manga.id === chapter.manga.id, ); const selected = !areNoItemsSelected ? selectedItemIds.includes(chapter.id) : null; return { chapter, downloadChapter, selected, }; }), [queue, selectedItemIds, visibleChapters], ); const chapterListFAB = useMemo(() => { const selectedChapters = chaptersWithMeta.filter((chapter) => chapter.selected); if (selectedChapters.length) { return ( {(handleClose) => ( )} ); } if (!isLatestChapterRead) { return ; } return null; }, [chaptersWithMeta, isLatestChapterRead]); if (isLoading || (noChaptersFound && isRefreshing)) { return (
); } return ( <> {`${visibleChapters.length} ${t('chapter.title', { count: visibleChapters.length, })}`} Chapters.markAsRead( ChaptersWithMeta.getChapters(ChaptersWithMeta.getNonRead(chaptersWithMeta)), true, ) } > Chapters.download( ChaptersWithMeta.getIds(ChaptersWithMeta.getNonDownloaded(chaptersWithMeta)), ) } > handleSelectAll(checked, checked ? chapters.map((chapter) => chapter.id) : []) } /> {noChaptersFound && } {noChaptersMatchingFilter && } }} totalCount={visibleChapters.length} itemContent={(index: number) => ( handleSelection(chaptersWithMeta[index].chapter.id, selected)} /> )} useWindowScroll={window.innerWidth < 900} overscan={window.innerHeight * 0.5} /> {chapterListFAB} ); };