Files
suwayomi-material-you-webui/src/screens/Updates.tsx

233 lines
9.3 KiB
TypeScript
Raw Normal View History

2021-09-28 00:41:12 +03:30
/*
* 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/.
*/
2021-09-28 00:41:12 +03:30
import DownloadIcon from '@mui/icons-material/Download';
import { Box, CardActionArea, styled } from '@mui/material';
import Avatar from '@mui/material/Avatar';
2021-09-28 00:41:12 +03:30
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
2023-10-04 22:34:32 +02:00
import React, { useCallback, useContext, useEffect, useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { t as translate } from 'i18next';
import { GroupedVirtuoso } from 'react-virtuoso';
2023-08-15 13:54:29 +02:00
import requestManager from '@/lib/requests/RequestManager.ts';
import LoadingPlaceholder from '@/components/util/LoadingPlaceholder';
import EmptyView from '@/components/util/EmptyView';
import DownloadStateIndicator from '@/components/molecules/DownloadStateIndicator';
import NavbarContext from '@/components/context/NavbarContext';
2023-10-04 22:34:32 +02:00
import { ChapterType, DownloadType } from '@/lib/graphql/generated/graphql.ts';
const StyledGroupedVirtuoso = styled(GroupedVirtuoso)(({ theme }) => ({
// 64px header
height: 'calc(100vh - 64px)',
[theme.breakpoints.down('sm')]: {
// 64px header (margin); 64px menu (margin);
height: 'calc(100vh - 64px - 64px)',
},
}));
const StyledGroupHeader = styled(Typography, { shouldForwardProp: (prop) => prop !== 'isFirstItem' })<{
isFirstItem: boolean;
}>(({ theme, isFirstItem }) => ({
paddingLeft: '24px',
// 16px - 10px (bottom padding of the group items)
paddingTop: '6px',
paddingBottom: '16px',
fontWeight: 700,
textTransform: 'uppercase',
backgroundColor: theme.palette.background.default,
[theme.breakpoints.down('sm')]: {
// 16px - 8px (margin of header)
paddingTop: isFirstItem ? '8px' : '6px',
},
}));
const StyledGroupItemWrapper = styled(Box, { shouldForwardProp: (prop) => prop !== 'isLastItem' })<{
isLastItem: boolean;
}>(({ isLastItem }) => ({
padding: '0 10px',
paddingBottom: isLastItem ? '0' : '10px',
}));
2021-09-28 00:41:12 +03:30
function epochToDate(epoch: number) {
const date = new Date(0); // The 0 there is the key, which sets the date to the epoch
date.setUTCSeconds(epoch);
return date;
}
function isTheSameDay(first: Date, second: Date) {
return (
first.getDate() === second.getDate() &&
first.getMonth() === second.getMonth() &&
first.getFullYear() === second.getFullYear()
);
2021-09-28 00:41:12 +03:30
}
function getDateString(date: Date) {
const today = new Date();
if (isTheSameDay(today, date)) return translate('global.date.label.today');
2021-09-28 00:41:12 +03:30
// calculate yesterday
const yesterday = new Date();
yesterday.setDate(today.getDate() - 1);
if (isTheSameDay(yesterday, date)) return translate('global.date.label.yesterday');
2021-09-28 00:41:12 +03:30
return date.toLocaleDateString();
}
const groupByDate = (updates: ChapterType[]): [date: string, items: number][] => {
if (!updates.length) {
return [];
}
2021-09-28 00:41:12 +03:30
const dateToItemMap = new Map<string, number>();
updates.forEach((item) => {
const date = getDateString(epochToDate(Number(item.fetchedAt)));
dateToItemMap.set(date, (dateToItemMap.get(date) ?? 0) + 1);
2021-09-28 00:41:12 +03:30
});
return [...dateToItemMap.entries()];
};
2021-09-28 00:41:12 +03:30
const Updates: React.FC = () => {
const { t } = useTranslation();
const location = useLocation();
2021-09-28 00:41:12 +03:30
const { setTitle, setAction } = useContext(NavbarContext);
const {
data: chapterUpdateData,
loading: isLoading,
fetchMore,
} = requestManager.useGetRecentlyUpdatedChapters(undefined, {
fetchPolicy: 'cache-and-network',
notifyOnNetworkStatusChange: true,
omitAbortSignal: true,
});
const hasNextPage = !!chapterUpdateData?.chapters.pageInfo.hasNextPage;
const endCursor = chapterUpdateData?.chapters.pageInfo.endCursor;
const updateEntries = (chapterUpdateData?.chapters.nodes as ChapterType[]) ?? [];
const groupedUpdates = useMemo(() => groupByDate(updateEntries), [updateEntries]);
const groupCounts: number[] = useMemo(() => groupedUpdates.map((group) => group[1]), [groupedUpdates]);
2023-10-04 22:34:32 +02:00
const { data: downloaderData } = requestManager.useDownloadSubscription();
const queue = (downloaderData?.downloadChanged.queue as DownloadType[]) ?? [];
2021-09-28 00:41:12 +03:30
useEffect(() => {
setTitle(t('updates.title'));
2021-09-28 00:41:12 +03:30
setAction(null);
}, [t]);
2021-09-28 00:41:12 +03:30
const downloadForChapter = (chapter: ChapterType) => {
const {
sourceOrder,
manga: { id: mangaId },
} = chapter;
2023-10-04 22:34:32 +02:00
return queue.find((q) => sourceOrder === q.chapter.sourceOrder && mangaId === q.chapter.manga.id);
2021-09-28 00:41:12 +03:30
};
const downloadChapter = (chapter: ChapterType) => {
2023-09-01 20:40:24 +02:00
requestManager.addChapterToDownloadQueue(chapter.id);
2021-09-28 00:41:12 +03:30
};
const loadMore = useCallback(() => {
if (!hasNextPage) {
return;
}
fetchMore({ variables: { offset: updateEntries.length } });
}, [hasNextPage, endCursor]);
if (!isLoading && updateEntries.length === 0) {
return <EmptyView message={t('updates.error.label.no_updates_available')} />;
}
2021-09-28 00:41:12 +03:30
return (
<StyledGroupedVirtuoso
style={{
// override Virtuoso default values and set them with class
height: 'undefined',
}}
components={{
Footer: () => (isLoading ? <LoadingPlaceholder usePadding /> : null),
}}
overscan={window.innerHeight * 0.5}
endReached={loadMore}
groupCounts={groupCounts}
groupContent={(index) => (
<StyledGroupHeader variant="h5" isFirstItem={index === 0}>
{groupedUpdates[index][0]}
</StyledGroupHeader>
)}
itemContent={(index) => {
const chapter = updateEntries[index];
const { manga } = chapter;
const download = downloadForChapter(chapter);
return (
<StyledGroupItemWrapper key={index} isLastItem={index === updateEntries.length - 1}>
<Card>
<CardActionArea
component={Link}
to={`/manga/${chapter.manga.id}/chapter/${chapter.sourceOrder}`}
state={location.state}
>
<CardContent
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: 2,
}}
>
<Box sx={{ display: 'flex' }}>
<Avatar
variant="rounded"
sx={{
width: 56,
height: 56,
flex: '0 0 auto',
marginRight: 2,
imageRendering: 'pixelated',
}}
src={requestManager.getValidImgUrlFor(manga.thumbnailUrl ?? '')}
/>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="h5" component="h2">
{manga.title}
</Typography>
<Typography variant="caption" display="block" gutterBottom>
{chapter.name}
</Typography>
</Box>
</Box>
{download && <DownloadStateIndicator download={download} />}
{download == null && !chapter.isDownloaded && (
<IconButton
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
downloadChapter(chapter);
}}
size="large"
>
<DownloadIcon />
</IconButton>
)}
</CardContent>
</CardActionArea>
</Card>
</StyledGroupItemWrapper>
);
}}
/>
2021-09-28 00:41:12 +03:30
);
};
export default Updates;