From b5f86ae6097d18048c5b4ef8fd5622460a32c31b Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Wed, 13 Dec 2023 01:55:38 +0100 Subject: [PATCH] Feature/virtualize download queue (#494) * Virtualize the download queue * Show loading placeholder for download queue --- src/screens/DownloadQueue.tsx | 187 +++++++++++++++++++++++----------- 1 file changed, 127 insertions(+), 60 deletions(-) diff --git a/src/screens/DownloadQueue.tsx b/src/screens/DownloadQueue.tsx index 88f7366c..a1195bae 100644 --- a/src/screens/DownloadQueue.tsx +++ b/src/screens/DownloadQueue.tsx @@ -13,24 +13,91 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import { Card, CardActionArea, Stack, Box, Tooltip } from '@mui/material'; import IconButton from '@mui/material/IconButton'; import React, { useContext, useEffect } from 'react'; -import { DragDropContext, Draggable } from 'react-beautiful-dnd'; +import { DragDropContext, Draggable, DraggableProvided } 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 { BoxProps } from '@mui/material/Box/Box'; 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 { EmptyView } from '@/components/util/EmptyView'; -import { DownloadType } from '@/lib/graphql/generated/graphql.ts'; +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'; + +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 { data: downloaderData } = requestManager.useDownloadSubscription(); + const { data: downloaderData, loading: isLoading } = requestManager.useDownloadSubscription(); const queue = (downloaderData?.downloadChanged.queue as DownloadType[]) ?? []; const status = downloaderData?.downloadChanged.state ?? 'STARTED'; const isQueueEmpty = !queue.length; @@ -72,6 +139,23 @@ export const DownloadQueue: React.FC = () => { ); }, [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 onDragEnd = () => {}; const handleDelete = async (chapter: TChapter) => { @@ -101,71 +185,54 @@ export const DownloadQueue: React.FC = () => { requestManager.startDownloads().response.catch(() => {}); }; + if (isLoading) { + return ; + } + if (isQueueEmpty) { return ; } return ( - + ( + + )} + > {(droppableProvided) => ( - {queue.map((item, index) => ( - - {(draggableProvided, snapshot) => ( - - - - - - - - {item.chapter.manga.title} - - {item.chapter.name} - - - - - { - e.preventDefault(); - e.stopPropagation(); - handleDelete(item.chapter); - }} - size="large" - > - - - - - - - )} - - ))} - {droppableProvided.placeholder} + ( + + {(draggableProvided) => ( + + )} + + )} + /> )}