import { useState, useEffect } from 'react'; import { useDownloadStore, DownloadItem } from '../store/useDownloadStore'; import { useDownloadProgressStore } from '../store/downloadStore'; import { useShallow } from 'zustand/react/shallow'; import { useSettingsStore } from '../store/useSettingsStore'; import { ChevronDown, ChevronRight, FolderPlus, Info, CheckCircle, AlertCircle, Play, Pause } from 'lucide-react'; import { open } from '@tauri-apps/plugin-dialog'; import { resolveCategoryDestination } from '../utils/downloadLocations'; import { isIdentityLocked as getIdentityLocked, isTransferLocked as getTransferLocked } from '../utils/downloadActions'; type LoginMode = 'matching' | 'custom' | 'none'; export const PropertiesModal = () => { const selectedPropertiesDownloadId = useDownloadStore(state => state.selectedPropertiesDownloadId); const setSelectedPropertiesDownloadId = useDownloadStore(state => state.setSelectedPropertiesDownloadId); const item = useDownloadStore(useShallow(state => selectedPropertiesDownloadId ? state.downloads.find(d => d.id === selectedPropertiesDownloadId) ?? null : null )); const liveProgress = useDownloadProgressStore(useShallow(state => selectedPropertiesDownloadId ? state.progressMap[selectedPropertiesDownloadId] : undefined )); const { baseDownloadFolder, perServerConnections } = useSettingsStore(); // Form states const [url, setUrl] = useState(''); const [fileName, setFileName] = useState(''); const [saveLocation, setSaveLocation] = useState(''); const [connections, setConnections] = useState(16); const [speedLimitEnabled, setSpeedLimitEnabled] = useState(false); const [speedLimitValue, setSpeedLimitValue] = useState('1024'); // KiB/s const [loginMode, setLoginMode] = useState('matching'); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [advancedExpanded, setAdvancedExpanded] = useState(false); const [checksumEnabled, setChecksumEnabled] = useState(false); const [checksumAlgorithm, setChecksumAlgorithm] = useState('SHA-256'); const [checksumValue, setChecksumValue] = useState(''); const [cookies, setCookies] = useState(''); const [headers, setHeaders] = useState(''); const [mirrors, setMirrors] = useState(''); const [errorMessage, setErrorMessage] = useState(''); useEffect(() => { if (selectedPropertiesDownloadId) { const activeItem = useDownloadStore.getState().downloads.find(d => d.id === selectedPropertiesDownloadId); if (activeItem) { setUrl(activeItem.url); setFileName(activeItem.fileName); if (activeItem.destination) { setSaveLocation(activeItem.destination); } else { void resolveCategoryDestination( useSettingsStore.getState(), activeItem.category ).then(setSaveLocation); } setConnections(activeItem.connections || 16); if (activeItem.speedLimit) { setSpeedLimitEnabled(true); setSpeedLimitValue(activeItem.speedLimit.replace(/[^0-9]/g, '')); } else { setSpeedLimitEnabled(false); } if (activeItem.username || activeItem.password) { setLoginMode('custom'); setUsername(activeItem.username || ''); setPassword(activeItem.password || ''); } else { setLoginMode('matching'); setUsername(''); setPassword(''); } setHeaders(activeItem.headers || ''); setChecksumEnabled(!!activeItem.checksum); if (activeItem.checksum) { const [algo, val] = activeItem.checksum.split('='); if (val) { setChecksumAlgorithm(algo); setChecksumValue(val); } } else { setChecksumAlgorithm('SHA-256'); setChecksumValue(''); } setCookies(activeItem.cookies || ''); setMirrors(activeItem.mirrors || ''); setErrorMessage(''); } else { setSelectedPropertiesDownloadId(null); } } }, [selectedPropertiesDownloadId, baseDownloadFolder, setSelectedPropertiesDownloadId]); if (!selectedPropertiesDownloadId || !item) return null; const handleBrowse = async () => { if (identityLocked) return; try { const selected = await open({ directory: true, multiple: false, defaultPath: saveLocation.startsWith('~') ? undefined : saveLocation }); if (selected && typeof selected === 'string') { setSaveLocation(selected); } } catch (e) { console.error("Failed to select folder:", e); } }; const handleSave = async () => { if (!url.trim()) { setErrorMessage("Enter a valid URL."); return; } if (!fileName.trim()) { setErrorMessage("File name cannot be empty."); return; } const updates: Partial = { url, fileName, destination: saveLocation, connections: Number(connections), speedLimit: speedLimitEnabled && speedLimitValue ? `${speedLimitValue}K` : undefined, username: loginMode === 'custom' ? username.trim() : undefined, password: loginMode === 'custom' ? password.trim() : undefined, headers: headers.trim() || undefined, checksum: checksumEnabled && checksumValue.trim() ? `${checksumAlgorithm}=${checksumValue.trim()}` : undefined, cookies: cookies.trim() || undefined, mirrors: mirrors.trim() || undefined, }; try { setErrorMessage(''); await useDownloadStore.getState().applyProperties(item.id, updates); setSelectedPropertiesDownloadId(null); } catch (e) { setErrorMessage(e instanceof Error ? e.message : String(e)); } }; const identityLocked = getIdentityLocked(item.status); const transferLocked = getTransferLocked(item.status); const displayedFraction = item.status === 'completed' ? 1 : liveProgress?.fraction ?? item.fraction ?? 0; const displayedSpeed = item.status === 'completed' ? '-' : liveProgress?.speed ?? item.speed ?? '-'; const displayedEta = item.status === 'completed' ? '-' : liveProgress?.eta ?? item.eta ?? '-'; let statusColor = 'text-text-secondary'; let StatusIcon = Info; if (item.status === 'completed') { statusColor = 'text-green-500'; StatusIcon = CheckCircle; } else if (item.status === 'downloading' || item.status === 'retrying') { statusColor = 'text-blue-500'; StatusIcon = Play; } else if (item.status === 'processing') { statusColor = 'text-sky-500'; StatusIcon = Play; } else if (item.status === 'paused') { statusColor = 'text-orange-500'; StatusIcon = Pause; } else if (item.status === 'failed') { statusColor = 'text-red-500'; StatusIcon = AlertCircle; } return (
{/* Header Summary */}

{item.fileName}

{item.status}
Progress{`${(displayedFraction * 100).toFixed(0)}%`}
Size{item.size || '-'}
Speed{displayedSpeed}
ETA{displayedEta}
Connections{item.connections || perServerConnections || '-'}
Speed cap{item.speedLimit || '-'}
Category{item.category}
Last try-
Date added{new Date(item.dateAdded).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })}
Destination{saveLocation || baseDownloadFolder}
{/* Scrollable Form Content */}
{identityLocked && (
{item.status === 'completed' ? : } {item.status === 'completed' ? "File identity is read-only. Transfer settings are saved for redownload." : "Transfer settings can be changed after stopping or pausing. Current transfers keep their existing backend options."}
)} {/* Download Section */}

Download

setUrl(e.target.value)} disabled={identityLocked} className="bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary font-mono focus:outline-none focus:border-accent disabled:opacity-50" /> setFileName(e.target.value)} disabled={identityLocked} className="bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-accent disabled:opacity-50" />
setConnections(Number(e.target.value))} disabled={transferLocked} className="w-16 bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-accent disabled:opacity-50" /> per file
{speedLimitEnabled && (
setSpeedLimitValue(e.target.value)} disabled={transferLocked} className="w-20 bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-accent disabled:opacity-50" /> KiB/s
)}
{/* Site Login Section */}

{item.status === 'completed' ? 'Site Login for Redownload' : 'Site Login'}

{(['matching', 'custom', 'none'] as const).map((mode) => ( ))}
{loginMode === 'matching' && (
Will use saved login if available.
)} {loginMode === 'custom' && ( <> setUsername(e.target.value)} disabled={transferLocked} placeholder="Username" className="max-w-[250px] bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-accent disabled:opacity-50" /> setPassword(e.target.value)} disabled={transferLocked} placeholder="Password" className="max-w-[250px] bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-accent disabled:opacity-50" /> )}
{/* Advanced Transfer Section */}
{advancedExpanded && (
{checksumEnabled && ( <> setChecksumValue(e.target.value)} disabled={transferLocked} placeholder="Expected digest" className="bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary font-mono focus:outline-none focus:border-accent disabled:opacity-50" /> )} setCookies(e.target.value)} disabled={transferLocked} placeholder="Cookies" className="bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary font-mono focus:outline-none focus:border-accent disabled:opacity-50" />
)}
{/* Footer */}
{errorMessage}
); };