diff --git a/src/modules/chapter/components/buttons/ChapterDownloadRetryButton.tsx b/src/modules/chapter/components/buttons/ChapterDownloadRetryButton.tsx index 2ec13893..eea48598 100644 --- a/src/modules/chapter/components/buttons/ChapterDownloadRetryButton.tsx +++ b/src/modules/chapter/components/buttons/ChapterDownloadRetryButton.tsx @@ -15,6 +15,7 @@ import { ChapterIdInfo, Chapters } from '@/modules/chapter/services/Chapters.ts' import { requestManager } from '@/lib/requests/RequestManager.ts'; import { makeToast } from '@/modules/core/utils/Toast.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { MUIUtil } from '@/lib/mui/MUI.util.ts'; export const ChapterDownloadRetryButton = ({ chapterId }: { chapterId: ChapterIdInfo['id'] }) => { const { t } = useTranslation(); @@ -35,6 +36,7 @@ export const ChapterDownloadRetryButton = ({ chapterId }: { chapterId: ChapterId return ( { e.preventDefault(); e.stopPropagation(); diff --git a/src/modules/downloads/components/DownloadQueueChapterCard.tsx b/src/modules/downloads/components/DownloadQueueChapterCard.tsx new file mode 100644 index 00000000..e42d923e --- /dev/null +++ b/src/modules/downloads/components/DownloadQueueChapterCard.tsx @@ -0,0 +1,97 @@ +/* + * 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 Card from '@mui/material/Card'; +import CardActionArea from '@mui/material/CardActionArea'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import { memo, useCallback } from 'react'; +import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { CustomTooltip } from '@/modules/core/components/CustomTooltip.tsx'; +import { ChapterDownloadRetryButton } from '@/modules/chapter/components/buttons/ChapterDownloadRetryButton.tsx'; +import { DownloadStateIndicator } from '@/modules/core/components/DownloadStateIndicator.tsx'; +import { ChapterCardMetadata } from '@/modules/chapter/components/cards/ChapterCardMetadata.tsx'; +import { MUIUtil } from '@/lib/mui/MUI.util.ts'; +import { ListCardContent } from '@/modules/core/components/cards/list/ListCardContent.tsx'; +import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; +import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; +import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { makeToast } from '@/modules/core/utils/Toast.ts'; +import { requestManager } from '@/lib/requests/RequestManager.ts'; +import { DownloaderState } from '@/lib/graphql/generated/graphql.ts'; +import { ChapterDownloadStatus, ChapterIdInfo } from '@/modules/chapter/services/Chapters.ts'; + +export const DownloadQueueChapterCard = memo( + ({ item, status }: { item: ChapterDownloadStatus; status: DownloaderState }) => { + const { t } = useTranslation(); + + const handleDelete = useCallback( + async (chapter: ChapterIdInfo) => { + const isRunning = status === DownloaderState.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', getErrorMessage(e)); + } + + if (!isRunning) { + return; + } + + requestManager + .startDownloads() + .response.catch(defaultPromiseErrorHandler('DownloadQueue::startDownloads')); + }, + [status], + ); + + return ( + + + + + + + + + + + + { + e.preventDefault(); + e.stopPropagation(); + handleDelete(item.chapter); + }} + > + + + + + + + + ); + }, +); diff --git a/src/modules/downloads/screens/DownloadQueue.tsx b/src/modules/downloads/screens/DownloadQueue.tsx index f4c139f1..97064dfa 100644 --- a/src/modules/downloads/screens/DownloadQueue.tsx +++ b/src/modules/downloads/screens/DownloadQueue.tsx @@ -6,115 +6,30 @@ * 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 from '@mui/material/Box'; import IconButton from '@mui/material/IconButton'; -import React, { memo, useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'; -import Typography from '@mui/material/Typography'; -import { Link } from 'react-router-dom'; +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 CardContent from '@mui/material/CardContent'; -import Refresh from '@mui/icons-material/Refresh'; 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 { DownloadStateIndicator } from '@/modules/core/components/DownloadStateIndicator.tsx'; 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, ChapterIdInfo } from '@/modules/chapter/services/Chapters.ts'; -import { DownloaderState, DownloadState } from '@/lib/graphql/generated/graphql.ts'; -import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; -import { getErrorMessage, noOp } from '@/lib/HelperFunctions.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 { MUIUtil } from '@/lib/mui/MUI.util.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'; - -const DownloadChapterItem = memo( - ({ - item, - handleDelete, - handleRetry, - }: { - item: ChapterDownloadStatus; - handleDelete: (chapter: ChapterIdInfo) => void; - handleRetry: (chapter: ChapterIdInfo) => void; - }) => { - const { t } = useTranslation(); - - return ( - - - - - - - - - - {item.manga.title} - - - {item.chapter.name} - - - - {item.state === DownloadState.Error && ( - - { - e.preventDefault(); - e.stopPropagation(); - handleRetry(item.chapter); - }} - > - - - - )} - - { - e.preventDefault(); - e.stopPropagation(); - handleDelete(item.chapter); - }} - > - - - - - - - - ); - }, -); +import { DownloadQueueChapterCard } from '@/modules/downloads/components/DownloadQueueChapterCard.tsx'; export const DownloadQueue: React.FC = () => { const { t } = useTranslation(); @@ -130,7 +45,7 @@ export const DownloadQueue: React.FC = () => { const downloaderData = downloadStatusData?.downloadStatus; const queue = downloaderData?.queue ?? []; - const status = downloaderData?.state ?? 'STARTED'; + const status = downloaderData?.state ?? DownloaderState.Started; const isQueueEmpty = !queue.length; const { setTitle, setAction } = useNavBarContext(); @@ -155,6 +70,31 @@ export const DownloadQueue: React.FC = () => { } }; + 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( @@ -199,66 +139,6 @@ export const DownloadQueue: React.FC = () => { return () => window.removeEventListener('error', ignoreError); }, []); - 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); - }; - - const handleRetry = useCallback(async (chapter: ChapterIdInfo) => { - try { - await requestManager.addChapterToDownloadQueue(chapter.id).response; - } catch (e) { - makeToast(t('download.queue.error.label.failed_to_remove'), 'error', getErrorMessage(e)); - } - }, []); - - const handleDelete = useCallback(async (chapter: ChapterIdInfo) => { - const isRunning = status === DownloaderState.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', getErrorMessage(e)); - } - - if (!isRunning) { - return; - } - - requestManager.startDownloads().response.catch(defaultPromiseErrorHandler('DownloadQueue::startDownloads')); - }, []); - if (isLoading) { return ; } @@ -278,39 +158,37 @@ export const DownloadQueue: React.FC = () => { } 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) => ( - - - - )} - /> - - - - - + + + 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) => ( + + + + )} + /> + + + + + + ); };