import React, { useState, useEffect, useRef } from 'react'; import { Inbox, Zap, CheckCircle2, CircleDashed, Film, Music, FileText, Box, Image as ImageIcon, Archive, FileQuestion, List, CalendarClock, Gauge, Bug, Settings, Plus, Play, Pause, Edit2, Trash2, PanelLeft, ChevronDown, type LucideIcon } from 'lucide-react'; import { useDownloadStore, DownloadCategory, Queue } from '../store/useDownloadStore'; import { ActiveView, useSettingsStore } from '../store/useSettingsStore'; import { WindowDragRegion } from './WindowDragRegion'; import { useToast } from '../contexts/ToastContext'; import { isTransferActiveStatus } from '../utils/downloads'; import { useTranslation } from 'react-i18next'; export type SidebarFilter = 'all' | 'active' | 'completed' | 'unfinished' | DownloadCategory | 'settings' | string; interface SidebarProps { selectedFilter: SidebarFilter; onSelectFilter: (filter: SidebarFilter) => void; } export const Sidebar: React.FC = (props) => { const { selectedFilter, onSelectFilter } = props; const { downloads, queues, addQueue, renameQueue, removeQueue, startQueue, pauseQueue } = useDownloadStore(); const { activeView, setActiveView, toggleSidebar } = useSettingsStore(); const { addToast } = useToast(); const { t } = useTranslation(); const [isAddingQueue, setIsAddingQueue] = useState(false); const [newQueueName, setNewQueueName] = useState(''); const [renamingQueueId, setRenamingQueueId] = useState(null); const [editingQueueName, setEditingQueueName] = useState(''); const [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null); const [foldersCollapsed, setFoldersCollapsed] = useState(() => window.localStorage.getItem('firelink-folders-collapsed') === 'true' ); const foldersToggleRef = useRef(null); const foldersListRef = useRef(null); const addInputRef = useRef(null); const renameInputRef = useRef(null); const addQueueSubmitRef = useRef(false); const addQueueCancelRef = useRef(false); const renameQueueSubmitRef = useRef(false); const renameQueueCancelRef = useRef(null); const renamingQueueIdRef = useRef(null); const editingQueueNameRef = useRef(''); const rejectedAddQueueNameRef = useRef(null); const rejectedRenameRef = useRef<{ queueId: string; name: string } | null>(null); 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(() => { if (isAddingQueue) addInputRef.current?.focus(); }, [isAddingQueue]); useEffect(() => { if (renamingQueueId) renameInputRef.current?.focus(); }, [renamingQueueId]); useEffect(() => { window.localStorage.setItem('firelink-folders-collapsed', String(foldersCollapsed)); }, [foldersCollapsed]); useEffect(() => { if (foldersCollapsed && foldersListRef.current?.contains(document.activeElement)) { foldersToggleRef.current?.focus(); } }, [foldersCollapsed]); const handleFoldersToggle = () => { if (foldersListRef.current?.contains(document.activeElement)) { foldersToggleRef.current?.focus(); } setFoldersCollapsed(collapsed => !collapsed); }; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (document.querySelector('.app-modal-backdrop') || document.querySelector('.app-modal')) return; const activeEl = document.activeElement as HTMLElement | null; const isInput = activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.isContentEditable); if (!isInput && (e.key === 'Delete' || e.key === 'Backspace')) { if (activeEl && activeEl.closest('.sidebar-inner')) { const focusedQueueId = activeEl .closest('[data-sidebar-queue-id]') ?.dataset.sidebarQueueId; if (activeView === 'downloads' && focusedQueueId) { const queueId = focusedQueueId; const q = queues.find(q => q.id === queueId); if (q && !q.isMain) { e.preventDefault(); if (!window.confirm(t($ => $.sidebar.deleteQueueConfirm, { name: q.name }))) { return; } void removeQueue(queueId).catch(error => { addToast({ message: t($ => $.sidebar.deleteQueueFailed, { detail: String(error) }), variant: 'error', isActionable: true }); }); } } } } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [addToast, activeView, queues, removeQueue]); const getCount = (filter: SidebarFilter) => { if (filter.startsWith('queue:')) { const qid = filter.replace('queue:', ''); return downloads.filter(d => d.queueId === qid && d.status !== 'completed').length; } switch (filter) { case 'all': return downloads.length; case 'active': return downloads.filter(d => isTransferActiveStatus(d.status)).length; case 'completed': return downloads.filter(d => d.status === 'completed').length; case 'unfinished': return downloads.filter(d => d.status !== 'completed').length; default: return downloads.filter(d => d.category === filter as DownloadCategory).length; } }; const NavItem = ({ icon: Icon, label, filter }: { icon: LucideIcon, label: string, filter: SidebarFilter }) => { const isSelected = activeView === 'downloads' && selectedFilter === filter; return ( ); }; const handleQueueContextMenu = (e: React.MouseEvent, id: string) => { e.preventDefault(); e.stopPropagation(); setContextMenu({ x: e.clientX, y: e.clientY, id }); }; const handleAddQueueSubmit = (trigger: 'submit' | 'blur' = 'submit') => { if (addQueueCancelRef.current) { addQueueCancelRef.current = false; return; } if (addQueueSubmitRef.current) return; const normalizedName = newQueueName.trim(); if (!normalizedName) { if (trigger === 'blur') { setNewQueueName(''); setIsAddingQueue(false); return; } addToast({ message: t($ => $.sidebar.queueNameEmpty), variant: 'error', isActionable: true }); return; } if (!addQueue(normalizedName)) { if (trigger === 'blur' && rejectedAddQueueNameRef.current === normalizedName) { rejectedAddQueueNameRef.current = null; setNewQueueName(''); setIsAddingQueue(false); return; } rejectedAddQueueNameRef.current = normalizedName; addToast({ message: t($ => $.sidebar.queueNameExists), variant: 'error', isActionable: true }); return; } rejectedAddQueueNameRef.current = null; addQueueSubmitRef.current = true; setNewQueueName(''); setIsAddingQueue(false); }; const handleRenameQueueSubmit = (queueId: string, trigger: 'submit' | 'blur' = 'submit') => { if (renameQueueCancelRef.current === queueId) { renameQueueCancelRef.current = null; return; } if (renamingQueueIdRef.current !== queueId) return; if (renameQueueSubmitRef.current) return; const normalizedName = editingQueueNameRef.current.trim(); if (!normalizedName) { if (trigger === 'blur') { renamingQueueIdRef.current = null; editingQueueNameRef.current = ''; setEditingQueueName(''); setRenamingQueueId(null); return; } addToast({ message: t($ => $.sidebar.queueNameEmpty), variant: 'error', isActionable: true }); return; } if (!renameQueue(queueId, normalizedName)) { if ( trigger === 'blur' && rejectedRenameRef.current?.queueId === queueId && rejectedRenameRef.current.name === normalizedName ) { rejectedRenameRef.current = null; renamingQueueIdRef.current = null; editingQueueNameRef.current = ''; setEditingQueueName(''); setRenamingQueueId(null); return; } rejectedRenameRef.current = { queueId, name: normalizedName }; addToast({ message: t($ => $.sidebar.queueNameExists), variant: 'error', isActionable: true }); return; } rejectedRenameRef.current = null; renameQueueSubmitRef.current = true; renamingQueueIdRef.current = null; setRenamingQueueId(null); }; const QueueItem = ({ queue }: { queue: Queue }) => { const filterId = `queue:${queue.id}`; const isSelected = activeView === 'downloads' && selectedFilter === filterId; const isRenaming = renamingQueueId === queue.id; if (isRenaming) { return (
{ editingQueueNameRef.current = e.target.value; rejectedRenameRef.current = null; setEditingQueueName(e.target.value); }} onKeyDown={e => { if (e.key === 'Enter') handleRenameQueueSubmit(queue.id); if (e.key === 'Escape') { e.preventDefault(); renameQueueCancelRef.current = queue.id; renamingQueueIdRef.current = null; editingQueueNameRef.current = ''; setEditingQueueName(''); setRenamingQueueId(null); } }} onBlur={() => handleRenameQueueSubmit(queue.id, 'blur')} />
); } return ( ); }; const ToolItem = ({ icon: Icon, label, view }: { icon: LucideIcon; label: string; view: ActiveView }) => { const isSelected = activeView === view; return ( ); }; return ( ); };