/* * 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 DownloadIcon from '@mui/icons-material/Download'; import Box from '@mui/material/Box'; import CardActionArea from '@mui/material/CardActionArea'; import Tooltip from '@mui/material/Tooltip'; import Avatar from '@mui/material/Avatar'; import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import IconButton from '@mui/material/IconButton'; import Typography from '@mui/material/Typography'; import React, { useCallback, useContext, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import Refresh from '@mui/icons-material/Refresh'; import { requestManager } from '@/lib/requests/requests/RequestManager.ts'; import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx'; import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx'; import { DownloadStateIndicator } from '@/modules/core/components/DownloadStateIndicator.tsx'; import { ChapterType, DownloadState } from '@/lib/graphql/generated/graphql.ts'; import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { UpdateChecker } from '@/modules/core/components/UpdateChecker.tsx'; import { StyledGroupedVirtuoso } from '@/modules/core/components/virtuoso/StyledGroupedVirtuoso.tsx'; import { StyledGroupHeader } from '@/modules/core/components/virtuoso/StyledGroupHeader.tsx'; import { StyledGroupItemWrapper } from '@/modules/core/components/virtuoso/StyledGroupItemWrapper.tsx'; import { Mangas } from '@/modules/manga/services/Mangas.ts'; import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx'; import { dateTimeFormatter, epochToDate, getDateString } from '@/util/DateHelper.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { TypographyMaxLines } from '@/modules/core/components/TypographyMaxLines.tsx'; import { ChapterIdInfo, ChapterMangaInfo } from '@/modules/chapter/services/Chapters.ts'; import { makeToast } from '@/lib/ui/Toast.ts'; const groupByDate = (updates: Pick[]): [date: string, items: number][] => { if (!updates.length) { return []; } const dateToItemMap = new Map(); updates.forEach((item) => { const date = getDateString(epochToDate(Number(item.fetchedAt))); dateToItemMap.set(date, (dateToItemMap.get(date) ?? 0) + 1); }); return [...dateToItemMap.entries()]; }; export const Updates: React.FC = () => { const { t } = useTranslation(); const location = useLocation(); const { setTitle, setAction } = useContext(NavBarContext); const { data: chapterUpdateData, loading: isLoading, error, fetchMore, refetch, } = requestManager.useGetRecentlyUpdatedChapters(undefined, { fetchPolicy: 'cache-and-network', notifyOnNetworkStatusChange: true, }); const hasNextPage = !!chapterUpdateData?.chapters.pageInfo.hasNextPage; const endCursor = chapterUpdateData?.chapters.pageInfo.endCursor; const updateEntries = chapterUpdateData?.chapters.nodes ?? []; const groupedUpdates = useMemo(() => groupByDate(updateEntries), [updateEntries]); const groupCounts: number[] = useMemo(() => groupedUpdates.map((group) => group[1]), [groupedUpdates]); const { data: downloaderData } = requestManager.useGetDownloadStatus(); const queue = downloaderData?.downloadStatus.queue ?? []; const lastUpdateTimestampCompRef = useRef(null); const [lastUpdateTimestampCompHeight, setLastUpdateTimestampCompHeight] = useState(0); useLayoutEffect(() => { setLastUpdateTimestampCompHeight(lastUpdateTimestampCompRef.current?.clientHeight ?? 0); }, [lastUpdateTimestampCompRef.current]); const { data: lastUpdateTimestampData } = requestManager.useGetLastGlobalUpdateTimestamp({ /** * The {@link UpdateChecker} is responsible for updating the timestamp */ fetchPolicy: 'cache-only', }); const lastUpdateTimestamp = lastUpdateTimestampData?.lastUpdateTimestamp.timestamp; useLayoutEffect(() => { setTitle(t('updates.title')); setAction(); return () => { setTitle(''); setAction(null); }; }, [t, lastUpdateTimestamp]); const downloadForChapter = (chapter: Pick & ChapterMangaInfo) => { const { sourceOrder, mangaId } = chapter; return queue.find((q) => sourceOrder === q.chapter.sourceOrder && mangaId === q.manga.id); }; const handleRetry = async (chapter: ChapterIdInfo) => { try { await requestManager.addChapterToDownloadQueue(chapter.id).response; } catch (e) { makeToast(t('download.queue.error.label.failed_to_remove'), 'error'); } }; const downloadChapter = (chapter: ChapterIdInfo) => { requestManager .addChapterToDownloadQueue(chapter.id) .response.catch(() => makeToast(t('global.error.label.failed_to_save_changes'), 'error')); }; const loadMore = useCallback(() => { if (!hasNextPage) { return; } fetchMore({ variables: { offset: updateEntries.length } }); }, [hasNextPage, endCursor]); if (error) { return ( refetch().catch(defaultPromiseErrorHandler('Updates::refetch'))} /> ); } if (!isLoading && updateEntries.length === 0) { return ; } return ( <> ({ [theme.breakpoints.up('sm')]: { paddingTop: '6px' } }), }} > {t('library.settings.global_update.label.last_update', { date: lastUpdateTimestamp ? dateTimeFormatter.format(+lastUpdateTimestamp) : '-', })} (isLoading ? : null), }} overscan={window.innerHeight * 0.5} endReached={loadMore} groupCounts={groupCounts} groupContent={(index) => ( {groupedUpdates[index][0]} )} itemContent={(index) => { const chapter = updateEntries[index]; const { manga } = chapter; const download = downloadForChapter(chapter); return ( theme.palette.text[chapter.isRead ? 'disabled' : 'primary'], }} > {manga.title} {chapter.name} {download && } {download?.state === DownloadState.Error && ( { e.preventDefault(); e.stopPropagation(); handleRetry(download.chapter); }} size="large" > )} {download == null && !chapter.isDownloaded && ( { e.stopPropagation(); e.preventDefault(); downloadChapter(chapter); }} size="large" > )} ); }} /> ); };