import React, { useState, useEffect } from 'react'; import { useDownloadStore, DownloadItem } from '../store/useDownloadStore'; import { useToast } from '../contexts/ToastContext'; import { useSettingsStore } from '../store/useSettingsStore'; import { SidebarFilter } from './Sidebar'; import { Play, Pause, Plus, FileText, Image as ImageIcon, Music, Film, Box, Archive, FileQuestion, PanelLeft, ArrowDownCircle, Command, ChevronRight } from 'lucide-react'; import { DownloadItem as DownloadItemComponent } from './DownloadItem'; import { invokeCommand as invoke } from '../ipc'; import { resolveCategoryDestination, resolveDownloadFilePath } from '../utils/downloadLocations'; import { canPauseDownload, canRedownload, canStartDownload, startActionLabel } from '../utils/downloadActions'; interface DownloadTableProps { filter: SidebarFilter; } const DEFAULT_COLUMN_WIDTHS = [340, 100, 220, 100, 80, 170]; const COLUMN_WIDTHS_STORAGE_KEY = 'firelink-download-column-widths'; export const DownloadTable: React.FC = ({ filter }) => { const { downloads, queues, assignToQueue, toggleAddModal, openDeleteModal, redownload } = useDownloadStore(); const { isSidebarVisible, toggleSidebar } = useSettingsStore(); const { addToast } = useToast(); const isMac = navigator.userAgent.includes('Mac'); const [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null); const [selectedIds, setSelectedIds] = useState>(new Set()); const [lastSelectedId, setLastSelectedId] = useState(null); const [columnWidths, setColumnWidths] = useState(() => { try { const stored = JSON.parse(window.localStorage.getItem(COLUMN_WIDTHS_STORAGE_KEY) || 'null'); return Array.isArray(stored) && stored.length === DEFAULT_COLUMN_WIDTHS.length && stored.every(value => typeof value === 'number' && Number.isFinite(value)) ? stored : DEFAULT_COLUMN_WIDTHS; } catch { return DEFAULT_COLUMN_WIDTHS; } }); const columnMinimums = [0, 58, 92, 58, 48, 112]; const tableGridTemplate = columnWidths.map((width, index) => `minmax(${columnMinimums[index]}px, ${width}fr)`).join(' '); const startColumnResize = (index: number, event: React.PointerEvent) => { event.preventDefault(); event.stopPropagation(); const startX = event.clientX; const startWidth = columnWidths[index]; const handlePointerMove = (moveEvent: PointerEvent) => { const nextWidth = Math.max(columnMinimums[index], startWidth + moveEvent.clientX - startX); setColumnWidths(widths => widths.map((width, columnIndex) => columnIndex === index ? nextWidth : width)); }; const handlePointerUp = () => { window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener('pointerup', handlePointerUp); document.body.classList.remove('is-resizing'); }; document.body.classList.add('is-resizing'); window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointerup', handlePointerUp); }; useEffect(() => { const handleCloseMenu = () => setContextMenu(null); const handleEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') setContextMenu(null); }; window.addEventListener('click', handleCloseMenu); window.addEventListener('keydown', handleEscape); return () => { window.removeEventListener('click', handleCloseMenu); window.removeEventListener('keydown', handleEscape); }; }, []); useEffect(() => { window.localStorage.setItem(COLUMN_WIDTHS_STORAGE_KEY, JSON.stringify(columnWidths)); }, [columnWidths]); const showInteractionError = (message: string, error: unknown) => { const detail = error instanceof Error ? error.message : String(error); addToast({ message: `${message}: ${detail}`, variant: 'error', isActionable: true }); }; const getDownloadPath = async (item: DownloadItem) => { const fileName = item.fileName?.trim(); if (!fileName) return null; const settings = useSettingsStore.getState(); const destination = item.destination || await resolveCategoryDestination(settings, item.category); return resolveDownloadFilePath(destination, fileName); }; const openProperties = (id: string) => { useDownloadStore.getState().setSelectedPropertiesDownloadId(id); }; const openDownloadFile = async (item: DownloadItem) => { if (item.status !== 'completed') { openProperties(item.id); return; } const fullPath = await getDownloadPath(item); if (!fullPath) { openProperties(item.id); return; } try { await invoke('open_downloaded_file', { path: fullPath }); } catch (error) { console.error("Failed to open file:", error); showInteractionError('Could not open downloaded file', error); } }; const revealDownloadFile = async (item: DownloadItem) => { const pathToReveal = await getDownloadPath(item); if (!pathToReveal) { openProperties(item.id); return; } try { await invoke('reveal_in_file_manager', { path: pathToReveal }); } catch (error) { console.error("Failed to show in Finder:", error); showInteractionError('Could not show download in Finder', error); } }; const handleDownloadDoubleClick = (item: DownloadItem) => { if (item.status === 'completed') { void openDownloadFile(item); return; } openProperties(item.id); }; const filteredDownloads = downloads.filter((d: DownloadItem) => { if (filter.startsWith('queue:')) { return d.queueId === filter.replace('queue:', '') && d.status !== 'completed'; } switch (filter) { case 'all': return true; case 'active': return d.status === 'downloading'; case 'completed': return d.status === 'completed'; case 'unfinished': return d.status !== 'completed'; default: return d.category === filter; } }); const handleItemClick = (e: React.MouseEvent, item: DownloadItem) => { if (e.metaKey || e.ctrlKey) { const newSelected = new Set(selectedIds); if (newSelected.has(item.id)) { newSelected.delete(item.id); } else { newSelected.add(item.id); } setSelectedIds(newSelected); setLastSelectedId(item.id); } else if (e.shiftKey && lastSelectedId) { const currentIndex = filteredDownloads.findIndex(d => d.id === item.id); const lastIndex = filteredDownloads.findIndex(d => d.id === lastSelectedId); if (currentIndex !== -1 && lastIndex !== -1) { const start = Math.min(currentIndex, lastIndex); const end = Math.max(currentIndex, lastIndex); const newSelected = new Set(selectedIds); for (let i = start; i <= end; i++) { newSelected.add(filteredDownloads[i].id); } setSelectedIds(newSelected); } } else { setSelectedIds(new Set([item.id])); setLastSelectedId(item.id); } }; const handleContextMenu = (menu: { x: number; y: number; id: string }) => { if (!selectedIds.has(menu.id)) { setSelectedIds(new Set([menu.id])); setLastSelectedId(menu.id); } setContextMenu(menu); }; const getFilterTitle = () => { if (filter.startsWith('queue:')) { const qid = filter.replace('queue:', ''); const queue = useDownloadStore.getState().queues.find(q => q.id === qid); return queue ? queue.name : 'Unknown Queue'; } switch (filter) { case 'all': return 'All Downloads'; case 'active': return 'Active'; case 'completed': return 'Completed'; case 'unfinished': return 'Unfinished'; default: return filter; } }; const handlePause = async (id: string) => { try { await invoke('pause_download', { id }); } catch (e) { console.error("Failed to pause:", e); showInteractionError('Could not pause download', e); } }; const handleResume = (item: DownloadItem) => { useDownloadStore.getState().resumeDownload(item.id); }; const handleDelete = (ids: string | string[]) => { openDeleteModal(ids); }; const contextItem = contextMenu ? downloads.find(d => d.id === contextMenu.id) : null; const getCategoryIcon = (category: string) => { switch(category) { case 'Musics': return ; case 'Movies': return ; case 'Documents': return ; case 'Applications': return ; case 'Pictures': return ; case 'Compressed': return ; case 'Other': return ; default: return ; } } return (
{!isSidebarVisible && ( )}
Firelink
{getFilterTitle()} {filteredDownloads.length}
{filteredDownloads.length === 0 ? (
) : ( <>
{['File Name', 'Size', 'Status', 'Speed', 'ETA', 'Date Added'].map((label, index) => (
{label}
startColumnResize(index, event)} />
))}
{filteredDownloads.map((d, index) => ( ))} {Array.from({ length: Math.max(0, 50 - filteredDownloads.length) }).map((_, i) => { const globalIndex = filteredDownloads.length + i; return (
); })}
)}
{/* Floating Context Menu */} {contextMenu && contextItem && (
e.stopPropagation()} > {selectedIds.size > 1 ? ( <> {/* Multi-Select Context Menu */}
{queues.map(q => ( ))}
) : ( <> {/* Single-Select Context Menu */} {contextItem.status === 'completed' && ( )}
{canPauseDownload(contextItem.status) && ( )} {canStartDownload(contextItem.status) && ( )} {canRedownload(contextItem.status) && ( )} {contextItem.status !== 'completed' && (
{queues.map(q => ( ))}
)}
{contextItem.status === 'completed' && ( )}
)}
)}
); };