mirror of
https://github.com/nimbold/Firelink.git
synced 2026-09-04 06:55:23 +00:00
perf(downloads): scale playlist rows efficiently
This commit is contained in:
@@ -1,11 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
|
||||||
import { useDownloadStore } from '../store/useDownloadStore';
|
|
||||||
import { useDownloadProgressStore } from '../store/downloadProgressStore';
|
import { useDownloadProgressStore } from '../store/downloadProgressStore';
|
||||||
import { Play, Pause, MoreVertical, Clock, ArrowUp, ArrowDown } from 'lucide-react';
|
import { Play, Pause, MoreVertical, Clock, ArrowUp, ArrowDown } from 'lucide-react';
|
||||||
import type { DownloadItem as DownloadItemType } from '../bindings/DownloadItem';
|
import type { DownloadItem as DownloadItemType } from '../bindings/DownloadItem';
|
||||||
import { canPauseDownload, canStartDownload, startActionLabel } from '../utils/downloadActions';
|
import { canPauseDownload, canStartDownload, startActionLabel } from '../utils/downloadActions';
|
||||||
import { isActiveDownloadStatus } from '../utils/downloads';
|
|
||||||
import {
|
import {
|
||||||
downloadProgressColorClass,
|
downloadProgressColorClass,
|
||||||
formatDownloadTotal,
|
formatDownloadTotal,
|
||||||
@@ -13,47 +10,35 @@ import {
|
|||||||
} from '../utils/downloadProgress';
|
} from '../utils/downloadProgress';
|
||||||
|
|
||||||
interface DownloadItemProps {
|
interface DownloadItemProps {
|
||||||
downloadId: string;
|
download: DownloadItemType;
|
||||||
index: number;
|
index: number;
|
||||||
|
queueIndex: number;
|
||||||
|
queueLength: number;
|
||||||
tableGridTemplate: string;
|
tableGridTemplate: string;
|
||||||
setContextMenu: (menu: { x: number; y: number; id: string }) => void;
|
setContextMenu: (menu: { x: number; y: number; id: string }) => void;
|
||||||
handlePause: (id: string, skipConfirm?: boolean) => void;
|
handlePause: (id: string, skipConfirm?: boolean) => void;
|
||||||
handleResume: (item: DownloadItemType) => void;
|
handleResume: (item: DownloadItemType) => void;
|
||||||
getCategoryIcon: (category: string) => React.ReactNode;
|
getCategoryIcon: (category: string) => React.ReactNode;
|
||||||
selectedIds: Set<string>;
|
isSelected: boolean;
|
||||||
|
onMoveInQueue: (id: string, direction: 'up' | 'down') => void;
|
||||||
onClick: (e: React.MouseEvent, item: DownloadItemType) => void;
|
onClick: (e: React.MouseEvent, item: DownloadItemType) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DownloadItem = React.memo<DownloadItemProps>(({
|
export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||||
downloadId,
|
download,
|
||||||
index,
|
index,
|
||||||
|
queueIndex,
|
||||||
|
queueLength,
|
||||||
tableGridTemplate,
|
tableGridTemplate,
|
||||||
setContextMenu,
|
setContextMenu,
|
||||||
handlePause,
|
handlePause,
|
||||||
handleResume,
|
handleResume,
|
||||||
getCategoryIcon,
|
getCategoryIcon,
|
||||||
selectedIds,
|
isSelected,
|
||||||
|
onMoveInQueue,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
const download = useDownloadStore(state => state.downloads.find(d => d.id === downloadId));
|
const liveProgress = useDownloadProgressStore(state => state.progressMap[download.id]);
|
||||||
const queueItems = useDownloadStore(useShallow(state => {
|
|
||||||
const item = state.downloads.find(candidate => candidate.id === downloadId);
|
|
||||||
if (!item) return [];
|
|
||||||
const queueId = item.queueId;
|
|
||||||
return state.downloads
|
|
||||||
.filter(candidate =>
|
|
||||||
candidate.queueId === queueId &&
|
|
||||||
candidate.status !== 'completed' &&
|
|
||||||
!(isActiveDownloadStatus(candidate.status) && candidate.status !== 'queued')
|
|
||||||
)
|
|
||||||
.sort((left, right) => (left.queuePosition ?? 0) - (right.queuePosition ?? 0))
|
|
||||||
.map(candidate => candidate.id);
|
|
||||||
}));
|
|
||||||
const moveInQueue = useDownloadStore(state => state.moveInQueue);
|
|
||||||
const liveProgress = useDownloadProgressStore(state => state.progressMap[downloadId]);
|
|
||||||
const queueIndex = queueItems.indexOf(downloadId);
|
|
||||||
|
|
||||||
if (!download) return null;
|
|
||||||
|
|
||||||
const displayFraction = download.status === 'downloading'
|
const displayFraction = download.status === 'downloading'
|
||||||
? liveProgress?.fraction ?? download.fraction ?? 0
|
? liveProgress?.fraction ?? download.fraction ?? 0
|
||||||
@@ -83,7 +68,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`download-row group cursor-default relative ${index % 2 !== 0 ? 'striped' : ''} ${selectedIds.has(downloadId) ? 'is-selected' : ''}`}
|
className={`download-row group cursor-default relative ${index % 2 !== 0 ? 'striped' : ''} ${isSelected ? 'is-selected' : ''}`}
|
||||||
style={{ gridTemplateColumns: tableGridTemplate }}
|
style={{ gridTemplateColumns: tableGridTemplate }}
|
||||||
onClick={(e) => onClick(e, download)}
|
onClick={(e) => onClick(e, download)}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
@@ -138,8 +123,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
style={{ width: `${displayFraction * 100}%` }}
|
style={{ width: `${displayFraction * 100}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
key={`status-${download.status}`}
|
|
||||||
title={
|
title={
|
||||||
download.lastError && (download.status === 'failed' || download.status === 'retrying')
|
download.lastError && (download.status === 'failed' || download.status === 'retrying')
|
||||||
? download.lastError
|
? download.lastError
|
||||||
@@ -181,7 +165,6 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
|
|
||||||
<div className="download-cell-truncate">
|
<div className="download-cell-truncate">
|
||||||
<span
|
<span
|
||||||
key={`speed-${download.status}`}
|
|
||||||
className="tabular-nums"
|
className="tabular-nums"
|
||||||
title={displaySpeed}
|
title={displaySpeed}
|
||||||
>
|
>
|
||||||
@@ -191,7 +174,6 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
|
|
||||||
<div className="download-cell-truncate">
|
<div className="download-cell-truncate">
|
||||||
<span
|
<span
|
||||||
key={`eta-${download.status}`}
|
|
||||||
className="tabular-nums"
|
className="tabular-nums"
|
||||||
title={displayEta}
|
title={displayEta}
|
||||||
>
|
>
|
||||||
@@ -215,7 +197,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
{(download.status === 'queued' || download.status === 'staged') && queueIndex !== -1 && (
|
{(download.status === 'queued' || download.status === 'staged') && queueIndex !== -1 && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => moveInQueue(selectedIds.has(download.id) ? Array.from(selectedIds) : download.id, 'up')}
|
onClick={() => onMoveInQueue(download.id, 'up')}
|
||||||
disabled={queueIndex === 0}
|
disabled={queueIndex === 0}
|
||||||
className="app-icon-button h-7 w-7 disabled:opacity-40"
|
className="app-icon-button h-7 w-7 disabled:opacity-40"
|
||||||
title="Move Up"
|
title="Move Up"
|
||||||
@@ -223,8 +205,8 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
<ArrowUp size={14} />
|
<ArrowUp size={14} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => moveInQueue(selectedIds.has(download.id) ? Array.from(selectedIds) : download.id, 'down')}
|
onClick={() => onMoveInQueue(download.id, 'down')}
|
||||||
disabled={queueIndex === queueItems.length - 1}
|
disabled={queueIndex === queueLength - 1}
|
||||||
className="app-icon-button h-7 w-7 disabled:opacity-40"
|
className="app-icon-button h-7 w-7 disabled:opacity-40"
|
||||||
title="Move Down"
|
title="Move Down"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
|
||||||
import { useDownloadStore, DownloadItem } from '../store/useDownloadStore';
|
import { useDownloadStore, DownloadItem } from '../store/useDownloadStore';
|
||||||
import { useToast } from '../contexts/ToastContext';
|
import { useToast } from '../contexts/ToastContext';
|
||||||
import { useSettingsStore } from '../store/useSettingsStore';
|
import { useSettingsStore } from '../store/useSettingsStore';
|
||||||
@@ -33,7 +33,7 @@ const DEFAULT_COLUMN_WIDTHS = [340, 100, 220, 100, 80, 170];
|
|||||||
const COLUMN_WIDTHS_STORAGE_KEY = 'firelink-download-column-widths';
|
const COLUMN_WIDTHS_STORAGE_KEY = 'firelink-download-column-widths';
|
||||||
|
|
||||||
export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
||||||
const { downloads, queues, assignToQueue, openDeleteModal, redownload } = useDownloadStore();
|
const { downloads, queues, assignToQueue, openDeleteModal, redownload, moveInQueue } = useDownloadStore();
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
const isMac = navigator.userAgent.includes('Mac');
|
const isMac = navigator.userAgent.includes('Mac');
|
||||||
const [isReadingClipboard, setIsReadingClipboard] = useState(false);
|
const [isReadingClipboard, setIsReadingClipboard] = useState(false);
|
||||||
@@ -54,8 +54,10 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
const [sortConfig, setSortConfig] = useState<DownloadSortConfig>({ column: 'Date Added', direction: 'desc' });
|
const [sortConfig, setSortConfig] = useState<DownloadSortConfig>({ column: 'Date Added', direction: 'desc' });
|
||||||
const [queueSortConfig, setQueueSortConfig] = useState<DownloadSortConfig | null>(null);
|
const [queueSortConfig, setQueueSortConfig] = useState<DownloadSortConfig | null>(null);
|
||||||
const selectedIdsRef = useRef(selectedIds);
|
const selectedIdsRef = useRef(selectedIds);
|
||||||
|
const lastSelectedIdRef = useRef(lastSelectedId);
|
||||||
const sortedDownloadsRef = useRef<DownloadItem[]>([]);
|
const sortedDownloadsRef = useRef<DownloadItem[]>([]);
|
||||||
selectedIdsRef.current = selectedIds;
|
selectedIdsRef.current = selectedIds;
|
||||||
|
lastSelectedIdRef.current = lastSelectedId;
|
||||||
const [columnWidths, setColumnWidths] = useState(() => {
|
const [columnWidths, setColumnWidths] = useState(() => {
|
||||||
try {
|
try {
|
||||||
const stored = JSON.parse(window.localStorage.getItem(COLUMN_WIDTHS_STORAGE_KEY) || 'null');
|
const stored = JSON.parse(window.localStorage.getItem(COLUMN_WIDTHS_STORAGE_KEY) || 'null');
|
||||||
@@ -138,25 +140,25 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const showInteractionError = (message: string, error: unknown) => {
|
const showInteractionError = useCallback((message: string, error: unknown) => {
|
||||||
const detail = error instanceof Error ? error.message : String(error);
|
const detail = error instanceof Error ? error.message : String(error);
|
||||||
addToast({ message: `${message}: ${detail}`, variant: 'error', isActionable: true });
|
addToast({ message: `${message}: ${detail}`, variant: 'error', isActionable: true });
|
||||||
};
|
}, [addToast]);
|
||||||
|
|
||||||
const getDownloadPath = async (item: DownloadItem) => {
|
const getDownloadPath = useCallback(async (item: DownloadItem) => {
|
||||||
const fileName = item.fileName?.trim();
|
const fileName = item.fileName?.trim();
|
||||||
if (!fileName) return null;
|
if (!fileName) return null;
|
||||||
const settings = useSettingsStore.getState();
|
const settings = useSettingsStore.getState();
|
||||||
const destination = item.destination ||
|
const destination = item.destination ||
|
||||||
await resolveCategoryDestination(settings, item.category);
|
await resolveCategoryDestination(settings, item.category);
|
||||||
return resolveDownloadFilePath(destination, fileName);
|
return resolveDownloadFilePath(destination, fileName);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const openProperties = (id: string) => {
|
const openProperties = useCallback((id: string) => {
|
||||||
useDownloadStore.getState().setSelectedPropertiesDownloadId(id);
|
useDownloadStore.getState().setSelectedPropertiesDownloadId(id);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const openDownloadFile = async (item: DownloadItem) => {
|
const openDownloadFile = useCallback(async (item: DownloadItem) => {
|
||||||
if (item.status !== 'completed') {
|
if (item.status !== 'completed') {
|
||||||
openProperties(item.id);
|
openProperties(item.id);
|
||||||
return;
|
return;
|
||||||
@@ -174,7 +176,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
console.error("Failed to open file:", error);
|
console.error("Failed to open file:", error);
|
||||||
showInteractionError('Could not open downloaded file', error);
|
showInteractionError('Could not open downloaded file', error);
|
||||||
}
|
}
|
||||||
};
|
}, [getDownloadPath, openProperties, showInteractionError]);
|
||||||
|
|
||||||
const revealDownloadFile = async (item: DownloadItem) => {
|
const revealDownloadFile = async (item: DownloadItem) => {
|
||||||
const pathToReveal = await getDownloadPath(item);
|
const pathToReveal = await getDownloadPath(item);
|
||||||
@@ -192,14 +194,14 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownloadDoubleClick = (item: DownloadItem) => {
|
const handleDownloadDoubleClick = useCallback((item: DownloadItem) => {
|
||||||
if (item.status === 'completed') {
|
if (item.status === 'completed') {
|
||||||
void openDownloadFile(item);
|
void openDownloadFile(item);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
openProperties(item.id);
|
openProperties(item.id);
|
||||||
};
|
}, [openDownloadFile, openProperties]);
|
||||||
|
|
||||||
const isQueueFilter = filter.startsWith('queue:');
|
const isQueueFilter = filter.startsWith('queue:');
|
||||||
const filteredDownloads = useMemo(() => downloads.filter((d: DownloadItem) => {
|
const filteredDownloads = useMemo(() => downloads.filter((d: DownloadItem) => {
|
||||||
@@ -232,6 +234,36 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
})
|
})
|
||||||
: sortDownloads(filteredDownloads, isQueueFilter ? queueSortConfig! : sortConfig),
|
: sortDownloads(filteredDownloads, isQueueFilter ? queueSortConfig! : sortConfig),
|
||||||
[filteredDownloads, isQueueFilter, queueSortConfig, sortConfig]);
|
[filteredDownloads, isQueueFilter, queueSortConfig, sortConfig]);
|
||||||
|
|
||||||
|
// Each row used to derive this by filtering and sorting the complete store
|
||||||
|
// independently. That made a 1000-entry playlist perform O(n^2 log n) work
|
||||||
|
// on every download update. Compute the same queue membership once and pass
|
||||||
|
// the resulting position to rows instead.
|
||||||
|
const queuePositionsByDownloadId = useMemo(() => {
|
||||||
|
const grouped = new Map<string | undefined, DownloadItem[]>();
|
||||||
|
for (const download of downloads) {
|
||||||
|
if (
|
||||||
|
download.status === 'completed'
|
||||||
|
|| (isActiveDownloadStatus(download.status) && download.status !== 'queued')
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const queueItems = grouped.get(download.queueId) || [];
|
||||||
|
queueItems.push(download);
|
||||||
|
grouped.set(download.queueId, queueItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
const positions = new Map<string, { index: number; length: number }>();
|
||||||
|
for (const queueItems of grouped.values()) {
|
||||||
|
queueItems.sort((left, right) =>
|
||||||
|
(left.queuePosition ?? 0) - (right.queuePosition ?? 0)
|
||||||
|
);
|
||||||
|
queueItems.forEach((download, index) => {
|
||||||
|
positions.set(download.id, { index, length: queueItems.length });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return positions;
|
||||||
|
}, [downloads]);
|
||||||
sortedDownloadsRef.current = sortedDownloads;
|
sortedDownloadsRef.current = sortedDownloads;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -246,27 +278,30 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setQueueSortConfig(null);
|
setQueueSortConfig(null);
|
||||||
}, [filter, isQueueFilter]);
|
}, [filter, isQueueFilter]);
|
||||||
const handleItemClick = (e: React.MouseEvent, item: DownloadItem) => {
|
const handleItemClick = useCallback((e: React.MouseEvent, item: DownloadItem) => {
|
||||||
if (e.detail === 2) {
|
if (e.detail === 2) {
|
||||||
handleDownloadDoubleClick(item);
|
handleDownloadDoubleClick(item);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.shiftKey && lastSelectedId) {
|
const currentSortedDownloads = sortedDownloadsRef.current;
|
||||||
const currentIndex = sortedDownloads.findIndex(d => d.id === item.id);
|
const currentSelectedIds = selectedIdsRef.current;
|
||||||
const lastIndex = sortedDownloads.findIndex(d => d.id === lastSelectedId);
|
const currentLastSelectedId = lastSelectedIdRef.current;
|
||||||
|
if (e.shiftKey && currentLastSelectedId) {
|
||||||
|
const currentIndex = currentSortedDownloads.findIndex(d => d.id === item.id);
|
||||||
|
const lastIndex = currentSortedDownloads.findIndex(d => d.id === currentLastSelectedId);
|
||||||
|
|
||||||
if (currentIndex !== -1 && lastIndex !== -1) {
|
if (currentIndex !== -1 && lastIndex !== -1) {
|
||||||
const start = Math.min(currentIndex, lastIndex);
|
const start = Math.min(currentIndex, lastIndex);
|
||||||
const end = Math.max(currentIndex, lastIndex);
|
const end = Math.max(currentIndex, lastIndex);
|
||||||
|
|
||||||
const newSelected = (e.metaKey || e.ctrlKey) ? new Set(selectedIds) : new Set<string>();
|
const newSelected = (e.metaKey || e.ctrlKey) ? new Set(currentSelectedIds) : new Set<string>();
|
||||||
for (let i = start; i <= end; i++) {
|
for (let i = start; i <= end; i++) {
|
||||||
newSelected.add(sortedDownloads[i].id);
|
newSelected.add(currentSortedDownloads[i].id);
|
||||||
}
|
}
|
||||||
setSelectedIds(newSelected);
|
setSelectedIds(newSelected);
|
||||||
}
|
}
|
||||||
} else if (e.metaKey || e.ctrlKey) {
|
} else if (e.metaKey || e.ctrlKey) {
|
||||||
const newSelected = new Set(selectedIds);
|
const newSelected = new Set(currentSelectedIds);
|
||||||
if (newSelected.has(item.id)) {
|
if (newSelected.has(item.id)) {
|
||||||
newSelected.delete(item.id);
|
newSelected.delete(item.id);
|
||||||
} else {
|
} else {
|
||||||
@@ -278,15 +313,22 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
setSelectedIds(new Set([item.id]));
|
setSelectedIds(new Set([item.id]));
|
||||||
setLastSelectedId(item.id);
|
setLastSelectedId(item.id);
|
||||||
}
|
}
|
||||||
};
|
}, [handleDownloadDoubleClick]);
|
||||||
|
|
||||||
const handleContextMenu = (menu: { x: number; y: number; id: string }) => {
|
const handleContextMenu = useCallback((menu: { x: number; y: number; id: string }) => {
|
||||||
if (!selectedIds.has(menu.id)) {
|
if (!selectedIdsRef.current.has(menu.id)) {
|
||||||
setSelectedIds(new Set([menu.id]));
|
setSelectedIds(new Set([menu.id]));
|
||||||
setLastSelectedId(menu.id);
|
setLastSelectedId(menu.id);
|
||||||
}
|
}
|
||||||
setContextMenu(menu);
|
setContextMenu(menu);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
|
const handleMoveInQueue = useCallback((id: string, direction: 'up' | 'down') => {
|
||||||
|
const ids = selectedIdsRef.current.has(id)
|
||||||
|
? Array.from(selectedIdsRef.current)
|
||||||
|
: id;
|
||||||
|
void moveInQueue(ids, direction);
|
||||||
|
}, [moveInQueue]);
|
||||||
|
|
||||||
const handleSort = (column: DownloadSortColumn) => {
|
const handleSort = (column: DownloadSortColumn) => {
|
||||||
const update = (current: DownloadSortConfig | null): DownloadSortConfig =>
|
const update = (current: DownloadSortConfig | null): DownloadSortConfig =>
|
||||||
@@ -317,7 +359,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePause = async (id: string, skipConfirm = false) => {
|
const handlePause = useCallback(async (id: string, skipConfirm = false) => {
|
||||||
const download = useDownloadStore.getState().downloads.find(d => d.id === id);
|
const download = useDownloadStore.getState().downloads.find(d => d.id === id);
|
||||||
if (!skipConfirm && download && download.resumable === false) {
|
if (!skipConfirm && download && download.resumable === false) {
|
||||||
const confirmPause = window.confirm("This download does not support resuming. If you pause it, you will have to start over again later. Are you sure you want to pause?");
|
const confirmPause = window.confirm("This download does not support resuming. If you pause it, you will have to start over again later. Are you sure you want to pause?");
|
||||||
@@ -332,9 +374,9 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
console.error("Failed to pause:", e);
|
console.error("Failed to pause:", e);
|
||||||
showInteractionError('Could not pause download', e);
|
showInteractionError('Could not pause download', e);
|
||||||
}
|
}
|
||||||
};
|
}, [showInteractionError]);
|
||||||
|
|
||||||
const handleResume = async (item: DownloadItem) => {
|
const handleResume = useCallback(async (item: DownloadItem) => {
|
||||||
try {
|
try {
|
||||||
const resumed = await useDownloadStore.getState().resumeDownload(item.id);
|
const resumed = await useDownloadStore.getState().resumeDownload(item.id);
|
||||||
if (!resumed) {
|
if (!resumed) {
|
||||||
@@ -344,7 +386,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
console.error("Failed to resume:", error);
|
console.error("Failed to resume:", error);
|
||||||
showInteractionError(`Could not resume ${item.fileName}`, error);
|
showInteractionError(`Could not resume ${item.fileName}`, error);
|
||||||
}
|
}
|
||||||
};
|
}, [showInteractionError]);
|
||||||
|
|
||||||
const resumeItemsSequentially = async (items: DownloadItem[]) => {
|
const resumeItemsSequentially = async (items: DownloadItem[]) => {
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
@@ -408,7 +450,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const getCategoryIcon = (category: string) => {
|
const getCategoryIcon = useCallback((category: string) => {
|
||||||
switch(category) {
|
switch(category) {
|
||||||
case 'Musics': return <Music size={16} className="text-pink-400" />;
|
case 'Musics': return <Music size={16} className="text-pink-400" />;
|
||||||
case 'Movies': return <Film size={16} className="text-red-400" />;
|
case 'Movies': return <Film size={16} className="text-red-400" />;
|
||||||
@@ -419,7 +461,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
case 'Other': return <FileQuestion size={16} className="text-gray-400" />;
|
case 'Other': return <FileQuestion size={16} className="text-gray-400" />;
|
||||||
default: return <FileQuestion size={16} className="text-gray-400" />;
|
default: return <FileQuestion size={16} className="text-gray-400" />;
|
||||||
}
|
}
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="downloads-view flex-1 flex flex-col h-full min-w-0">
|
<div className="downloads-view flex-1 flex flex-col h-full min-w-0">
|
||||||
@@ -544,14 +586,17 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
|||||||
{sortedDownloads.map((d, index) => (
|
{sortedDownloads.map((d, index) => (
|
||||||
<DownloadItemComponent
|
<DownloadItemComponent
|
||||||
key={d.id}
|
key={d.id}
|
||||||
downloadId={d.id}
|
download={d}
|
||||||
index={index}
|
index={index}
|
||||||
|
queueIndex={queuePositionsByDownloadId.get(d.id)?.index ?? -1}
|
||||||
|
queueLength={queuePositionsByDownloadId.get(d.id)?.length ?? 0}
|
||||||
tableGridTemplate={tableGridTemplate}
|
tableGridTemplate={tableGridTemplate}
|
||||||
setContextMenu={handleContextMenu}
|
setContextMenu={handleContextMenu}
|
||||||
handlePause={handlePause}
|
handlePause={handlePause}
|
||||||
handleResume={handleResume}
|
handleResume={handleResume}
|
||||||
getCategoryIcon={getCategoryIcon}
|
getCategoryIcon={getCategoryIcon}
|
||||||
selectedIds={selectedIds}
|
isSelected={selectedIds.has(d.id)}
|
||||||
|
onMoveInQueue={handleMoveInQueue}
|
||||||
onClick={handleItemClick}
|
onClick={handleItemClick}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user