2022-11-09 18:09:15 +01:00
|
|
|
/*
|
|
|
|
|
* 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
|
2023-05-18 13:17:41 +02:00
|
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
|
*/
|
2022-11-09 18:09:15 +01:00
|
|
|
|
2024-04-14 15:50:12 +02:00
|
|
|
import Box from '@mui/material/Box';
|
|
|
|
|
import Stack from '@mui/material/Stack';
|
|
|
|
|
import Tooltip from '@mui/material/Tooltip';
|
|
|
|
|
import { styled } from '@mui/material/styles';
|
2022-11-09 18:09:15 +01:00
|
|
|
import Typography from '@mui/material/Typography';
|
2024-08-30 15:50:17 +02:00
|
|
|
import { ComponentProps, useCallback, useMemo, useState } from 'react';
|
2022-11-09 18:09:15 +01:00
|
|
|
import { Virtuoso } from 'react-virtuoso';
|
2023-03-23 13:59:32 +01:00
|
|
|
import { useTranslation } from 'react-i18next';
|
2023-12-18 22:13:43 +01:00
|
|
|
import IconButton from '@mui/material/IconButton';
|
|
|
|
|
import DownloadIcon from '@mui/icons-material/Download';
|
2023-12-18 22:25:25 +01:00
|
|
|
import DoneAllIcon from '@mui/icons-material/DoneAll';
|
2024-04-20 01:40:41 +02:00
|
|
|
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
|
|
|
|
|
import Menu from '@mui/material/Menu';
|
2024-10-05 16:09:34 +02:00
|
|
|
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
2023-12-30 14:30:04 +01:00
|
|
|
import { ChapterCard } from '@/components/chapter/ChapterCard.tsx';
|
|
|
|
|
import { ResumeFab } from '@/components/manga/ResumeFAB.tsx';
|
|
|
|
|
import { filterAndSortChapters, useChapterOptions } from '@/components/chapter/util.tsx';
|
2024-10-05 16:09:34 +02:00
|
|
|
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
2023-12-30 14:30:04 +01:00
|
|
|
import { ChaptersToolbarMenu } from '@/components/chapter/ChaptersToolbarMenu.tsx';
|
|
|
|
|
import { SelectionFAB } from '@/components/collection/SelectionFAB.tsx';
|
2024-10-05 16:09:34 +02:00
|
|
|
import { DEFAULT_FULL_FAB_HEIGHT } from '@/modules/core/components/buttons/StyledFab.tsx';
|
2024-07-02 16:20:28 +02:00
|
|
|
import {
|
|
|
|
|
GetChaptersMangaQuery,
|
|
|
|
|
GetChaptersMangaQueryVariables,
|
2024-07-08 18:02:50 +02:00
|
|
|
MangaScreenFieldsFragment,
|
2024-07-02 16:20:28 +02:00
|
|
|
} from '@/lib/graphql/generated/graphql.ts';
|
2023-12-25 21:20:00 +01:00
|
|
|
import { useSelectableCollection } from '@/components/collection/useSelectableCollection.ts';
|
|
|
|
|
import { SelectableCollectionSelectAll } from '@/components/collection/SelectableCollectionSelectAll.tsx';
|
2023-12-30 04:04:59 +01:00
|
|
|
import { Chapters } from '@/lib/data/Chapters.ts';
|
2024-07-11 17:41:38 +02:00
|
|
|
import { ChaptersWithMeta, ChapterWithMetaType } from '@/lib/data/ChaptersWithMeta.ts';
|
2023-12-30 14:30:04 +01:00
|
|
|
import { ChapterActionMenuItems } from '@/components/chapter/ChapterActionMenuItems.tsx';
|
2024-04-20 01:40:41 +02:00
|
|
|
import { ChaptersDownloadActionMenuItems } from '@/components/chapter/ChaptersDownloadActionMenuItems.tsx';
|
2024-10-05 16:09:34 +02:00
|
|
|
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
|
|
|
|
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
|
2024-07-02 16:20:28 +02:00
|
|
|
import { GET_CHAPTERS_MANGA } from '@/lib/graphql/queries/ChapterQuery.ts';
|
2024-08-14 14:26:25 +02:00
|
|
|
import { Mangas } from '@/lib/data/Mangas';
|
2024-08-30 15:50:17 +02:00
|
|
|
import { useNavBarContext } from '@/components/context/NavbarContext.tsx';
|
2024-10-05 16:09:34 +02:00
|
|
|
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
2024-08-30 15:50:17 +02:00
|
|
|
import { MediaQuery } from '@/lib/ui/MediaQuery.tsx';
|
2024-10-05 16:09:34 +02:00
|
|
|
import { shouldForwardProp } from '@/modules/core/utils/ShouldForwardProp.ts';
|
2022-11-09 18:09:15 +01:00
|
|
|
|
2024-09-27 20:34:32 +02:00
|
|
|
type ChapterListHeaderProps = {
|
2024-09-06 23:50:38 +02:00
|
|
|
scrollbarWidth: number;
|
2024-09-27 20:34:32 +02:00
|
|
|
};
|
|
|
|
|
const ChapterListHeader = styled(Stack, {
|
|
|
|
|
shouldForwardProp: shouldForwardProp<ChapterListHeaderProps>(['scrollbarWidth']),
|
|
|
|
|
})<ChapterListHeaderProps>(({ theme, scrollbarWidth }) => ({
|
2024-08-30 15:50:17 +02:00
|
|
|
padding: theme.spacing(1),
|
2024-09-06 23:50:38 +02:00
|
|
|
paddingRight: `calc(${scrollbarWidth}px + ${theme.spacing(1)})`,
|
2024-08-30 15:50:17 +02:00
|
|
|
paddingBottom: 0,
|
2023-12-18 21:40:56 +01:00
|
|
|
[theme.breakpoints.down('md')]: {
|
2024-08-30 15:50:17 +02:00
|
|
|
paddingRight: theme.spacing(1),
|
2023-12-18 21:40:56 +01:00
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
2024-09-27 20:34:32 +02:00
|
|
|
type StyledVirtuosoProps = { topOffset: number };
|
|
|
|
|
const StyledVirtuoso = styled(Virtuoso, {
|
|
|
|
|
shouldForwardProp: shouldForwardProp<StyledVirtuosoProps>(['topOffset']),
|
|
|
|
|
})<StyledVirtuosoProps>(({ theme, topOffset }) => ({
|
|
|
|
|
listStyle: 'none',
|
|
|
|
|
padding: 0,
|
|
|
|
|
[theme.breakpoints.up('md')]: {
|
|
|
|
|
height: `calc(100vh - ${topOffset}px)`,
|
|
|
|
|
margin: 0,
|
|
|
|
|
},
|
|
|
|
|
}));
|
2022-11-09 18:09:15 +01:00
|
|
|
|
2024-07-11 17:41:38 +02:00
|
|
|
export interface IChapterWithMeta extends ChapterWithMetaType<ComponentProps<typeof ChapterCard>['chapter']> {
|
2023-02-06 10:06:33 +01:00
|
|
|
selected: boolean | null;
|
2022-11-21 01:33:21 +01:00
|
|
|
}
|
|
|
|
|
|
2024-07-11 17:41:38 +02:00
|
|
|
export const ChapterList = ({
|
|
|
|
|
manga,
|
|
|
|
|
isRefreshing,
|
|
|
|
|
}: {
|
2024-08-14 14:26:25 +02:00
|
|
|
manga: Pick<MangaScreenFieldsFragment, 'id' | 'firstUnreadChapter' | 'chapters' | 'unreadCount' | 'downloadCount'>;
|
2023-09-30 16:55:35 +02:00
|
|
|
isRefreshing: boolean;
|
2024-07-11 17:41:38 +02:00
|
|
|
}) => {
|
2023-03-23 13:59:32 +01:00
|
|
|
const { t } = useTranslation();
|
2024-08-30 15:50:17 +02:00
|
|
|
const { appBarHeight } = useNavBarContext();
|
|
|
|
|
|
|
|
|
|
const isMobileWidth = MediaQuery.useIsBelowWidth('md');
|
|
|
|
|
|
2024-09-01 01:39:13 +02:00
|
|
|
const [chapterListHeaderHeight, setChapterListHeaderHeight] = useState(50);
|
2024-08-30 15:50:17 +02:00
|
|
|
const [chapterListHeaderRef, setChapterListHeaderRef] = useState<HTMLDivElement | null>(null);
|
|
|
|
|
useResizeObserver(
|
|
|
|
|
chapterListHeaderRef,
|
|
|
|
|
useCallback(() => setChapterListHeaderHeight(chapterListHeaderRef?.offsetHeight ?? 0), [chapterListHeaderRef]),
|
|
|
|
|
);
|
2023-03-23 13:59:32 +01:00
|
|
|
|
2024-09-06 23:50:38 +02:00
|
|
|
const scrollbarWidth = MediaQuery.useGetScrollbarSize('width');
|
|
|
|
|
|
2024-01-01 22:40:39 +01:00
|
|
|
const { data: downloaderData } = requestManager.useGetDownloadStatus();
|
2024-07-11 17:41:38 +02:00
|
|
|
const queue = downloaderData?.downloadStatus.queue ?? [];
|
2022-11-09 18:09:15 +01:00
|
|
|
|
2023-09-30 16:55:35 +02:00
|
|
|
const [options, dispatch] = useChapterOptions(manga.id);
|
2024-04-27 22:28:55 +02:00
|
|
|
const {
|
|
|
|
|
data: chaptersData,
|
|
|
|
|
loading: isLoading,
|
|
|
|
|
error,
|
|
|
|
|
refetch,
|
2024-07-02 16:20:28 +02:00
|
|
|
} = requestManager.useGetMangaChapters<GetChaptersMangaQuery, GetChaptersMangaQueryVariables>(
|
|
|
|
|
GET_CHAPTERS_MANGA,
|
|
|
|
|
manga.id,
|
|
|
|
|
{ notifyOnNetworkStatusChange: true },
|
|
|
|
|
);
|
2023-10-15 16:03:08 +02:00
|
|
|
const chapters = useMemo(() => chaptersData?.chapters.nodes ?? [], [chaptersData?.chapters.nodes]);
|
2023-11-19 21:17:35 +01:00
|
|
|
|
2024-04-05 02:03:00 +02:00
|
|
|
const chapterIds = useMemo(() => chapters.map((chapter) => chapter.id), [chapters]);
|
|
|
|
|
|
2023-12-25 21:20:00 +01:00
|
|
|
const { areNoItemsSelected, areAllItemsSelected, selectedItemIds, handleSelectAll, handleSelection } =
|
2024-04-05 02:03:00 +02:00
|
|
|
useSelectableCollection(chapters.length, { itemIds: chapterIds, currentKey: 'default' });
|
2023-12-25 21:20:00 +01:00
|
|
|
|
2023-09-30 16:55:35 +02:00
|
|
|
const visibleChapters = useMemo(() => filterAndSortChapters(chapters, options), [chapters, options]);
|
2022-11-09 18:09:15 +01:00
|
|
|
|
2024-08-14 14:26:25 +02:00
|
|
|
const nextChapterIndexToRead = manga.firstUnreadChapter?.sourceOrder;
|
2022-11-09 18:09:15 +01:00
|
|
|
|
2024-08-14 14:26:25 +02:00
|
|
|
const areAllChaptersRead = Mangas.isFullyRead(manga);
|
|
|
|
|
const areAllChaptersDownloaded = Mangas.isFullyDownloaded(manga);
|
2023-12-18 22:13:43 +01:00
|
|
|
|
2023-09-30 16:55:35 +02:00
|
|
|
const noChaptersFound = chapters.length === 0;
|
|
|
|
|
const noChaptersMatchingFilter = !noChaptersFound && visibleChapters.length === 0;
|
|
|
|
|
|
|
|
|
|
const chaptersWithMeta: IChapterWithMeta[] = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
visibleChapters.map((chapter) => {
|
2024-07-11 17:41:38 +02:00
|
|
|
const downloadChapter = queue?.find((cd) => cd.chapter.id === chapter.id);
|
2023-12-25 21:20:00 +01:00
|
|
|
const selected = !areNoItemsSelected ? selectedItemIds.includes(chapter.id) : null;
|
2023-09-30 16:55:35 +02:00
|
|
|
return {
|
|
|
|
|
chapter,
|
|
|
|
|
downloadChapter,
|
|
|
|
|
selected,
|
|
|
|
|
};
|
|
|
|
|
}),
|
2023-12-25 21:20:00 +01:00
|
|
|
[queue, selectedItemIds, visibleChapters],
|
2023-09-30 16:55:35 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const chapterListFAB = useMemo(() => {
|
2023-12-25 21:20:00 +01:00
|
|
|
const selectedChapters = chaptersWithMeta.filter((chapter) => chapter.selected);
|
|
|
|
|
|
|
|
|
|
if (selectedChapters.length) {
|
|
|
|
|
return (
|
2024-07-01 19:51:27 +02:00
|
|
|
<SelectionFAB selectedItemsCount={selectedChapters.length} title="chapter.title_one">
|
2023-12-25 21:20:00 +01:00
|
|
|
{(handleClose) => (
|
2023-12-30 04:04:59 +01:00
|
|
|
<ChapterActionMenuItems selectedChapters={selectedChapters} onClose={handleClose} />
|
2023-12-25 21:20:00 +01:00
|
|
|
)}
|
|
|
|
|
</SelectionFAB>
|
|
|
|
|
);
|
2023-09-30 16:55:35 +02:00
|
|
|
}
|
|
|
|
|
|
2024-08-14 14:26:25 +02:00
|
|
|
if (nextChapterIndexToRead !== undefined) {
|
2023-09-30 16:55:35 +02:00
|
|
|
return <ResumeFab chapterIndex={nextChapterIndexToRead} mangaId={manga.id} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
2024-08-14 14:26:25 +02:00
|
|
|
}, [chaptersWithMeta]);
|
2023-09-30 16:55:35 +02:00
|
|
|
|
|
|
|
|
if (isLoading || (noChaptersFound && isRefreshing)) {
|
2022-11-09 18:09:15 +01:00
|
|
|
return (
|
2024-04-27 22:28:55 +02:00
|
|
|
<Stack sx={{ justifyContent: 'center', alignItems: 'center', position: 'relative', flexGrow: 1 }}>
|
|
|
|
|
<LoadingPlaceholder />
|
|
|
|
|
</Stack>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
return (
|
|
|
|
|
<Stack sx={{ justifyContent: 'center', position: 'relative', flexGrow: 1 }}>
|
2024-04-29 15:29:33 +02:00
|
|
|
<EmptyViewAbsoluteCentered
|
2024-04-27 22:28:55 +02:00
|
|
|
message={t('global.error.label.failed_to_load_data')}
|
|
|
|
|
messageExtra={error.message}
|
|
|
|
|
retry={() => refetch().catch(defaultPromiseErrorHandler('ChapterList::refetch'))}
|
|
|
|
|
/>
|
|
|
|
|
</Stack>
|
2022-11-09 18:09:15 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
2024-08-30 04:34:28 +02:00
|
|
|
<Stack direction="column" sx={{ position: 'relative', flexBasis: '60%' }}>
|
2024-08-30 15:50:17 +02:00
|
|
|
<ChapterListHeader
|
|
|
|
|
ref={setChapterListHeaderRef}
|
|
|
|
|
direction="row"
|
|
|
|
|
alignItems="center"
|
|
|
|
|
justifyContent="space-between"
|
2024-09-06 23:50:38 +02:00
|
|
|
scrollbarWidth={scrollbarWidth}
|
2024-08-30 15:50:17 +02:00
|
|
|
>
|
2024-08-30 15:52:02 +02:00
|
|
|
<Typography variant="h5" component="h3">
|
2024-07-01 19:51:27 +02:00
|
|
|
{`${visibleChapters.length} ${t('chapter.title_one', {
|
2023-03-23 13:59:32 +01:00
|
|
|
count: visibleChapters.length,
|
|
|
|
|
})}`}
|
2022-11-09 18:09:15 +01:00
|
|
|
</Typography>
|
|
|
|
|
|
2024-08-30 15:52:02 +02:00
|
|
|
<Stack direction="row">
|
2023-12-18 22:25:25 +01:00
|
|
|
<Tooltip title={t('chapter.action.mark_as_read.add.label.action.current')}>
|
|
|
|
|
<IconButton
|
|
|
|
|
disabled={areAllChaptersRead}
|
2023-12-30 04:04:59 +01:00
|
|
|
onClick={() =>
|
|
|
|
|
Chapters.markAsRead(
|
|
|
|
|
ChaptersWithMeta.getChapters(ChaptersWithMeta.getNonRead(chaptersWithMeta)),
|
|
|
|
|
true,
|
2024-04-21 03:44:02 +02:00
|
|
|
manga.id,
|
2023-12-30 04:04:59 +01:00
|
|
|
)
|
|
|
|
|
}
|
2023-12-18 22:25:25 +01:00
|
|
|
>
|
|
|
|
|
<DoneAllIcon />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</Tooltip>
|
2024-04-20 01:40:41 +02:00
|
|
|
<PopupState variant="popover" popupId="chapterlist-download-button">
|
|
|
|
|
{(popupState) => (
|
|
|
|
|
<>
|
|
|
|
|
<Tooltip title={t('chapter.action.download.add.label.action')}>
|
|
|
|
|
<IconButton disabled={areAllChaptersDownloaded} {...bindTrigger(popupState)}>
|
|
|
|
|
<DownloadIcon />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
{popupState.isOpen && (
|
|
|
|
|
<Menu {...bindMenu(popupState)}>
|
|
|
|
|
<ChaptersDownloadActionMenuItems
|
|
|
|
|
mangaIds={[manga.id]}
|
|
|
|
|
closeMenu={popupState.close}
|
|
|
|
|
/>
|
|
|
|
|
</Menu>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</PopupState>
|
|
|
|
|
|
2022-11-09 18:09:15 +01:00
|
|
|
<ChaptersToolbarMenu options={options} optionsDispatch={dispatch} />
|
2023-12-25 21:20:00 +01:00
|
|
|
<SelectableCollectionSelectAll
|
|
|
|
|
areAllItemsSelected={areAllItemsSelected}
|
|
|
|
|
areNoItemsSelected={areNoItemsSelected}
|
|
|
|
|
onChange={(checked) =>
|
|
|
|
|
handleSelectAll(checked, checked ? chapters.map((chapter) => chapter.id) : [])
|
|
|
|
|
}
|
|
|
|
|
/>
|
2023-12-18 21:40:56 +01:00
|
|
|
</Stack>
|
|
|
|
|
</ChapterListHeader>
|
2022-11-09 18:09:15 +01:00
|
|
|
|
2024-04-29 15:29:33 +02:00
|
|
|
{noChaptersFound && <EmptyViewAbsoluteCentered message={t('chapter.error.label.no_chapter_found')} />}
|
|
|
|
|
{noChaptersMatchingFilter && (
|
|
|
|
|
<EmptyViewAbsoluteCentered message={t('chapter.error.label.no_matches')} />
|
|
|
|
|
)}
|
2022-11-09 18:09:15 +01:00
|
|
|
|
|
|
|
|
<StyledVirtuoso
|
2024-08-30 15:50:17 +02:00
|
|
|
topOffset={appBarHeight + chapterListHeaderHeight}
|
2023-02-06 10:06:33 +01:00
|
|
|
style={{
|
|
|
|
|
// override Virtuoso default values and set them with class
|
2022-11-09 18:09:15 +01:00
|
|
|
height: 'undefined',
|
|
|
|
|
}}
|
2023-12-25 21:15:31 +01:00
|
|
|
components={{ Footer: () => <Box sx={{ paddingBottom: DEFAULT_FULL_FAB_HEIGHT }} /> }}
|
|
|
|
|
totalCount={visibleChapters.length}
|
|
|
|
|
itemContent={(index: number) => (
|
|
|
|
|
<ChapterCard
|
|
|
|
|
{...chaptersWithMeta[index]}
|
|
|
|
|
allChapters={chapters}
|
|
|
|
|
showChapterNumber={options.showChapterNumber}
|
2024-04-05 02:03:00 +02:00
|
|
|
onSelect={(selected, selectRange) =>
|
|
|
|
|
handleSelection(chaptersWithMeta[index].chapter.id, selected, { selectRange })
|
|
|
|
|
}
|
2023-12-25 21:15:31 +01:00
|
|
|
/>
|
|
|
|
|
)}
|
2024-08-30 15:50:17 +02:00
|
|
|
useWindowScroll={isMobileWidth}
|
2022-11-09 18:09:15 +01:00
|
|
|
overscan={window.innerHeight * 0.5}
|
|
|
|
|
/>
|
|
|
|
|
</Stack>
|
2023-09-30 16:55:35 +02:00
|
|
|
{chapterListFAB}
|
2022-11-09 18:09:15 +01:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|