/* * 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 PauseIcon from '@mui/icons-material/Pause'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import Box from '@mui/material/Box'; import IconButton from '@mui/material/IconButton'; import React, { useMemo, useState } from 'react'; import DeleteSweepIcon from '@mui/icons-material/DeleteSweep'; import type { DragEndEvent } from '@dnd-kit/core'; import { closestCenter, DndContext } from '@dnd-kit/core'; import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { useWindowEvent } from '@mantine/hooks'; import { useLingui } from '@lingui/react/macro'; import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { makeToast } from '@/base/utils/Toast.ts'; import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx'; import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { DownloaderState } from '@/lib/graphql/generated/graphql.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { DndSortableItem } from '@/lib/dnd-kit/DndSortableItem.tsx'; import { DndKitUtil } from '@/lib/dnd-kit/DndKitUtil.ts'; import { DndOverlayItem } from '@/lib/dnd-kit/DndOverlayItem.tsx'; import { DownloadQueueChapterCard } from '@/features/downloads/components/DownloadQueueChapterCard.tsx'; import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts'; import { useAppAction } from '@/features/navigation-bar/hooks/useAppAction.ts'; import type { ChapterDownloadStatus } from '@/features/chapter/Chapter.types.ts'; import { VirtuosoPersisted } from '@/lib/virtuoso/Component/VirtuosoPersisted.tsx'; import { STABLE_EMPTY_ARRAY } from '@/base/Base.constants.ts'; export const DownloadQueue: React.FC = () => { const { t } = useLingui(); useAppTitle(t`Download queue`); 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 ?? STABLE_EMPTY_ARRAY; const status = downloaderData?.state ?? DownloaderState.Started; const isQueueEmpty = !queue.length; const dndItems = useMemo(() => queue.map((download) => download.chapter), [queue]); const dndSensors = DndKitUtil.useSensorsForDevice(); const [dndActiveDownload, setDndActiveDownload] = useState(null); const clearQueue = async () => { try { await requestManager.clearDownloads().response; } catch (e) { makeToast(t`Could not remove all downloads from the queue`, 'error', getErrorMessage(e)); } }; const toggleQueueStatus = () => { if (status === DownloaderState.Stopped) { requestManager.startDownloads(); } else { requestManager.stopDownloads(); } }; const categoryReorder = (list: ChapterDownloadStatus[], from: number, to: number) => { if (from === to) { return; } reorderDownload({ variables: { input: { chapterId: list[from].chapter.id, to } } }).catch(() => { revertReorder(); }); }; const onDragEnd = (event: DragEndEvent) => { const { active, over } = event; setDndActiveDownload(null); if (!over || active.id === over.id) { return; } const oldIndex = queue.findIndex((download) => download.chapter.id === active.id); const newIndex = queue.findIndex((download) => download.chapter.id === over.id); categoryReorder(queue, oldIndex, newIndex); }; useAppAction( <> {status === DownloaderState.Stopped ? : } , [status, isQueueEmpty], ); // Virtuoso's resize observer can throw this error, // which is caught by DnD and aborts dragging. useWindowEvent('error', (e) => { if ( e.message === 'ResizeObserver loop completed with undelivered notifications.' || e.message === 'ResizeObserver loop limit exceeded' ) { e.stopImmediatePropagation(); } }); if (isLoading) { return ; } if (error) { return ( refetch().catch(defaultPromiseErrorHandler('DownloadQueue::refetch'))} /> ); } if (isQueueEmpty) { return ; } return ( setDndActiveDownload(queue.find((download) => download.chapter.id === event.active.id) ?? null) } onDragEnd={onDragEnd} onDragCancel={() => setDndActiveDownload(null)} onDragAbort={() => setDndActiveDownload(null)} > queue[index].chapter.id} itemContent={(index) => ( )} /> ); };