/* * 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 DeleteIcon from '@mui/icons-material/Delete'; import DragHandle from '@mui/icons-material/DragHandle'; import PauseIcon from '@mui/icons-material/Pause'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import Card from '@mui/material/Card'; import CardActionArea from '@mui/material/CardActionArea'; import Stack from '@mui/material/Stack'; import Box, { BoxProps } from '@mui/material/Box'; import Tooltip from '@mui/material/Tooltip'; import IconButton from '@mui/material/IconButton'; import React, { useContext, useEffect } from 'react'; import { DragDropContext, Draggable, DraggableProvided, DropResult } from 'react-beautiful-dnd'; import Typography from '@mui/material/Typography'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import DeleteSweepIcon from '@mui/icons-material/DeleteSweep'; import { Virtuoso } from 'react-virtuoso'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { StrictModeDroppable } from '@/lib/StrictModeDroppable'; import { makeToast } from '@/components/util/Toast'; import { DownloadStateIndicator } from '@/components/molecules/DownloadStateIndicator'; import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx'; import { ChapterType, DownloadType } from '@/lib/graphql/generated/graphql.ts'; import { TChapter } from '@/typings.ts'; import { NavBarContext } from '@/components/context/NavbarContext.tsx'; import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx'; import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts'; const HeightPreservingItem = ({ children, ...props }: BoxProps) => ( // the height is necessary to prevent the item container from collapsing, which confuses Virtuoso measurements {children} ); const DownloadChapterItem = ({ provided, item, isDragging, handleDelete, }: { provided: DraggableProvided; item: DownloadType; isDragging: boolean; handleDelete: (chapter: ChapterType) => void; }) => { const { t } = useTranslation(); return ( {item.chapter.manga.title} {item.chapter.name} { e.preventDefault(); e.stopPropagation(); handleDelete(item.chapter); }} size="large" > ); }; export const DownloadQueue: React.FC = () => { const { t } = useTranslation(); const [reorderDownload, { reset: revertReorder }] = requestManager.useReorderChapterInDownloadQueue(); const { data: downloadStatusData, loading: isLoading, error, refetch, } = requestManager.useGetDownloadStatus({ notifyOnNetworkStatusChange: true }); const downloaderData = downloadStatusData?.downloadStatus; const queue = (downloaderData?.queue as DownloadType[]) ?? []; const status = downloaderData?.state ?? 'STARTED'; const isQueueEmpty = !queue.length; const { setTitle, setAction } = useContext(NavBarContext); const clearQueue = async () => { try { await requestManager.clearDownloads().response; } catch (e) { makeToast(t('download.queue.error.label.failed_delete_all'), 'error'); } }; const toggleQueueStatus = () => { if (status === 'STOPPED') { requestManager.startDownloads(); } else { requestManager.stopDownloads(); } }; useEffect(() => { setTitle(t('download.queue.title')); setAction( <> {status === 'STOPPED' ? : } , ); return () => { setTitle(''); setAction(null); }; }, [t, status, isQueueEmpty]); useEffect(() => { const ignoreError = (e: WindowEventMap['error']) => { if ( e.message === 'ResizeObserver loop completed with undelivered notifications.' || e.message === 'ResizeObserver loop limit exceeded' ) { e.stopImmediatePropagation(); } }; // Virtuoso's resize observer can throw this error, // which is caught by DnD and aborts dragging. window.addEventListener('error', ignoreError); return () => window.removeEventListener('error', ignoreError); }, []); const categoryReorder = (list: DownloadType[], from: number, to: number) => { if (from === to) { return; } reorderDownload({ variables: { input: { chapterId: list[from].chapter.id, to } } }).catch(() => { revertReorder(); }); }; const onDragEnd = (result: DropResult) => { if (!result.destination) { return; } categoryReorder(queue, result.source.index, result.destination.index); }; const handleDelete = async (chapter: TChapter) => { const isRunning = status === 'STARTED'; try { if (isRunning) { // required to stop before deleting otherwise the download kept going. Server issue? await requestManager.stopDownloads().response; } await Promise.all([ // remove from download queue requestManager.removeChapterFromDownloadQueue(chapter.id).response, // delete partial download, should be handle server side? // bug: The folder and the last image downloaded are not deleted requestManager.deleteDownloadedChapter(chapter.id).response, ]); } catch (e) { makeToast(t('download.queue.error.label.failed_to_remove'), 'error'); } if (!isRunning) { return; } requestManager.startDownloads().response.catch(defaultPromiseErrorHandler('DownloadQueue::startDownloads')); }; if (isLoading) { return ; } if (error) { return ( refetch().catch(defaultPromiseErrorHandler('DownloadQueue::refetch'))} /> ); } if (isQueueEmpty) { return ; } return ( ( )} > {(droppableProvided) => ( ( {(draggableProvided) => ( )} )} /> )} ); };