import { useState, useEffect } from 'react'; import { useDownloadStore, DownloadItem } from '../store/useDownloadStore'; import { useDownloadProgressStore } from '../store/downloadProgressStore'; 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'; import { downloadProgressColorClass, formatDownloadTotal, resolveDownloadSizeDisplay } from '../utils/downloadProgress'; import { resolveDownloadConnections } from '../utils/downloads'; import { useTranslation } from 'react-i18next'; type LoginMode = 'matching' | 'custom' | 'none'; const formatLastTry = (value?: string): string => { if (!value) return '-'; const date = new Date(value); return Number.isNaN(date.getTime()) ? '-' : date.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' }); }; export const PropertiesModal = () => { const { t } = useTranslation(); const categoryLabel = (category: string) => { switch (category) { case 'Musics': return t($ => $.navigation.categories.musics); case 'Movies': return t($ => $.navigation.categories.movies); case 'Compressed': return t($ => $.navigation.categories.compressed); case 'Documents': return t($ => $.navigation.categories.documents); case 'Pictures': return t($ => $.navigation.categories.pictures); case 'Applications': return t($ => $.navigation.categories.applications); default: return t($ => $.navigation.categories.other); } }; 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(() => resolveDownloadConnections(undefined, perServerConnections)); const [connectionsDirty, setConnectionsDirty] = useState(false); 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(resolveDownloadConnections(activeItem.connections, perServerConnections)); setConnectionsDirty(false); 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, setSelectedPropertiesDownloadId]); useEffect(() => { if (!selectedPropertiesDownloadId || connectionsDirty) return; const activeItem = useDownloadStore.getState().downloads.find(d => d.id === selectedPropertiesDownloadId); if (activeItem && activeItem.connections === undefined) { setConnections(resolveDownloadConnections(undefined, perServerConnections)); } }, [selectedPropertiesDownloadId, perServerConnections, connectionsDirty]); useEffect(() => { if (!selectedPropertiesDownloadId) return; const handleEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') setSelectedPropertiesDownloadId(null); }; window.addEventListener('keydown', handleEscape); return () => window.removeEventListener('keydown', handleEscape); }, [selectedPropertiesDownloadId, 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(t($ => $.properties.enterValidUrl)); return; } if (!fileName.trim()) { setErrorMessage(t($ => $.properties.fileNameEmpty)); return; } const updates: Partial = { url, fileName, destination: saveLocation, 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, ...(connectionsDirty ? { connections: resolveDownloadConnections(connections, perServerConnections) } : {}), }; 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 configuredConnections = resolveDownloadConnections(item.connections, perServerConnections); const observedConnectionTotal = Math.max( 1, liveProgress?.requested_connections ?? configuredConnections ); const observedActiveConnections = liveProgress?.active_connections; const connectionTelemetryActive = item.status === 'downloading' || item.status === 'processing' || item.status === 'retrying'; const connectionStatus = (() => { if (item.isMedia) return t($ => $.properties.connectionsUnavailable); if (!connectionTelemetryActive) return String(configuredConnections); if (typeof observedActiveConnections === 'number') { return t($ => $.properties.connectionCount, { active: observedActiveConnections, total: observedConnectionTotal, }); } if (item.status === 'downloading') { return t($ => $.properties.connectionCountUnknown, { total: observedConnectionTotal }); } return t($ => $.properties.connectionCount, { active: 0, total: observedConnectionTotal, }); })(); 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 ?? '-'; const sizeDisplay = resolveDownloadSizeDisplay({ downloadedBytes: liveProgress?.downloaded_bytes ?? item.downloadedBytes, totalBytes: liveProgress?.total_bytes ?? item.totalBytes, totalIsEstimate: liveProgress?.total_is_estimate ?? item.totalIsEstimate, fallbackSize: item.size }); const hasDownloadedAmount = item.status !== 'completed' && Boolean(sizeDisplay.downloaded && sizeDisplay.total); const completedSizeLabel = (() => { const value = item.status === 'completed' ? formatDownloadTotal(sizeDisplay) : sizeDisplay.fallback; return value === 'Unknown' ? t($ => $.addDownloads.unknown) : value; })(); const statusLabel = t($ => $.downloads.status[item.status]); const sizeDescription = sizeDisplay.totalIsEstimate ? t($ => $.downloads.size.downloadedOfApproximate, { downloaded: sizeDisplay.downloaded ?? '', total: sizeDisplay.total ?? '', unit: sizeDisplay.unit ?? '', }) : t($ => $.downloads.size.downloadedOf, { downloaded: sizeDisplay.downloaded ?? '', total: sizeDisplay.total ?? '', unit: sizeDisplay.unit ?? '', }); 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 (
{ if (event.target === event.currentTarget) setSelectedPropertiesDownloadId(null); }} role="dialog" aria-modal="true" >
{/* Header Summary */}

{item.fileName}

{statusLabel}
{t($ => $.properties.progress)}{`${(displayedFraction * 100).toFixed(0)}%`}
{t($ => $.properties.size)} {hasDownloadedAmount ? ( <> {sizeDisplay.downloaded} / {sizeDisplay.totalIsEstimate ? '~' : ''}{sizeDisplay.total} {sizeDisplay.unit} ) : completedSizeLabel}
{t($ => $.properties.speed)}{displayedSpeed}
{t($ => $.properties.eta)}{displayedEta}
{t($ => $.properties.connections)} $.properties.savedTooltip) : t($ => $.properties.defaultTooltip)}>{connectionStatus}
{t($ => $.properties.speedCap)}{item.speedLimit || '-'}
{t($ => $.properties.category)}{categoryLabel(item.category)}
{t($ => $.properties.lastTry)}{formatLastTry(item.lastTry)}
{t($ => $.properties.dateAdded)}{new Date(item.dateAdded).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })}
{t($ => $.properties.destination)}{saveLocation || baseDownloadFolder}
{item.lastError && (item.status === 'failed' || item.status === 'retrying') && (
{t($ => $.properties.lastError)} {item.lastError}
)}
{/* Scrollable Form Content */}
{identityLocked && (
{item.status === 'completed' ? : } {item.status === 'completed' ? t($ => $.properties.identityReadOnly) : t($ => $.properties.transferSettings)}
)} {/* Download Section */}

{t($ => $.properties.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)); setConnectionsDirty(true); }} 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" /> {t($ => $.properties.perFile)} {connectionStatus} {!transferLocked && item.connections !== undefined && item.connections !== perServerConnections && ( )}
{t($ => $.properties.savedPerDownload)}
{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' ? t($ => $.properties.siteLoginRedownload) : t($ => $.properties.siteLogin)}

{(['matching', 'custom', 'none'] as const).map((mode) => ( ))}
{loginMode === 'matching' && (
{t($ => $.properties.useSavedLogin)}
)} {loginMode === 'custom' && ( <> setUsername(e.target.value)} disabled={transferLocked} placeholder={t($ => $.properties.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={t($ => $.properties.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={t($ => $.properties.expectedDigest)} 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} autoComplete="off" placeholder={t($ => $.properties.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}
); };