/* * 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, { useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import DeleteSweepIcon from '@mui/icons-material/DeleteSweep'; import { Virtuoso } from 'react-virtuoso'; import { closestCenter, DndContext, DragEndEvent } from '@dnd-kit/core'; import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { CustomTooltip } from '@/modules/core/components/CustomTooltip.tsx'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { makeToast } from '@/modules/core/utils/Toast.ts'; import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx'; import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { ChapterDownloadStatus } from '@/modules/chapter/services/Chapters.ts'; import { DownloaderState } from '@/lib/graphql/generated/graphql.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; 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 '@/modules/downloads/components/DownloadQueueChapterCard.tsx'; 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 ?? []; const status = downloaderData?.state ?? DownloaderState.Started; const isQueueEmpty = !queue.length; const { setTitle, setAction } = useNavBarContext(); 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('download.queue.error.label.failed_delete_all'), '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); }; useLayoutEffect(() => { setTitle(t('download.queue.title')); setAction( <> {status === DownloaderState.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); }, []); 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) => ( )} /> ); };