feat(ui): modernize add download window

This commit is contained in:
NimBold
2026-06-19 18:39:28 +03:30
parent 6db21b7f14
commit d921876b72
2 changed files with 433 additions and 60 deletions
+86 -60
View File
@@ -687,15 +687,30 @@ export const AddDownloadsModal = () => {
icon: LucideIcon;
color: string;
}) => (
<div className="flex flex-col bg-bg-input/50 border border-border-modal/40 rounded-lg p-2.5 shadow-sm">
<div className="add-download-summary-card flex flex-col">
<div className="flex items-center gap-1.5 text-text-muted mb-1">
<Icon size={12} className={color} />
<span className="text-[10px] font-bold uppercase tracking-wider">{title}</span>
<span className="text-[10px] font-semibold uppercase tracking-wider">{title}</span>
</div>
<span className="text-sm font-semibold text-text-primary truncate">{value}</span>
</div>
);
const selectMediaFormat = (index: number) => {
if (selectedItemIndex === null) return;
const newItems = [...parsedItems];
const selectedItem = newItems[selectedItemIndex];
const format = selectedItem?.formats?.[index];
if (!selectedItem || !format) return;
selectedItem.selectedFormat = index;
selectedItem.size = format.detail || 'Unknown';
selectedItem.sizeBytes = format.bytes || 0;
const baseName = selectedItem.file.substring(0, selectedItem.file.lastIndexOf('.')) || selectedItem.file;
selectedItem.file = `${baseName}.${format.ext}`;
setParsedItems(newItems);
};
const requiredBytes = parsedItems.reduce((acc, item) => acc + (item.sizeBytes || 0), 0);
const requiredStr = requiredBytes > 0
? (requiredBytes < 1024 * 1024 ? `${(requiredBytes / 1024).toFixed(1)} KB`
@@ -716,24 +731,24 @@ export const AddDownloadsModal = () => {
/>
)}
<div className="app-modal-backdrop fixed inset-0 z-50 flex items-center justify-center">
<div className="app-modal w-[900px] h-[650px] flex flex-col overflow-hidden text-sm">
<div className="app-modal add-download-modal w-[900px] h-[650px] flex flex-col overflow-hidden text-sm">
{/* Main Content Split */}
<div className="flex flex-1 overflow-hidden">
{/* Left Column: URLs and Preview */}
<div className="w-[55%] border-r border-border-modal flex flex-col bg-main-bg/50">
<div className="p-5 flex-1 flex flex-col gap-5">
<div className="add-download-left w-[55%] border-r border-border-modal flex flex-col">
<div className="add-download-pane p-5 flex-1 flex flex-col gap-5">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-text-primary font-semibold">
<div className="add-download-section-title flex items-center gap-2">
<Link size={16} className="text-blue-500" />
Download Links
</div>
</div>
<textarea
className="w-full h-32 bg-bg-input/80 border border-border-modal rounded-lg p-3 text-[13px] text-text-primary focus:outline-none focus:border-accent resize-none font-mono shadow-inner transition-colors"
className="add-download-control add-download-links-input w-full h-32 p-3 text-[13px] resize-none font-mono"
placeholder="Paste HTTP, HTTPS, FTP, or SFTP URLs here..."
value={urls}
onChange={(e) => setUrls(e.target.value)}
@@ -743,7 +758,7 @@ export const AddDownloadsModal = () => {
<button
type="button"
onClick={() => setMetadataRefreshNonce(value => value + 1)}
className="flex items-center gap-1.5 text-[11px] text-blue-500 hover:text-blue-400 font-medium"
className="add-download-link-button flex items-center gap-1.5 text-[11px] font-medium"
>
<RefreshCw size={12} /> Refresh Metadata
</button>
@@ -758,19 +773,19 @@ export const AddDownloadsModal = () => {
</div>
<div className="flex flex-col gap-2 flex-1 overflow-hidden">
<div className="flex items-center gap-2 text-text-primary font-semibold">
<div className="add-download-section-title flex items-center gap-2">
<ArrowRight size={16} className="text-blue-500" />
Preview
</div>
<div className="flex-1 border border-border-modal rounded-lg overflow-hidden bg-bg-input/30 flex flex-col">
<div className="bg-sidebar-bg/50 border-b border-border-modal px-3 py-2 flex text-[11px] font-semibold text-text-muted uppercase tracking-wider">
<div className="add-download-preview flex-1 overflow-hidden flex flex-col">
<div className="add-download-preview-header px-3 py-2 flex text-[11px] font-semibold text-text-muted uppercase tracking-wider">
<div className="flex-[2]">File</div>
<div className="flex-1">Size</div>
<div className="flex-[1.5]">Status</div>
</div>
<div className="flex-1 overflow-y-auto p-2 space-y-1">
<div className="flex-1 overflow-y-auto p-2 space-y-1" role="listbox" aria-label="Download preview">
{parsedItems.length === 0 ? (
<div className="h-full flex items-center justify-center text-text-muted text-xs italic">
<div className="add-download-empty h-full flex items-center justify-center text-text-muted text-xs">
No links added yet.
</div>
) : (
@@ -778,10 +793,19 @@ export const AddDownloadsModal = () => {
<div
key={i}
onClick={() => setSelectedItemIndex(i)}
className={`flex flex-col text-xs px-2 py-2 cursor-pointer rounded-md transition-all group ${
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
setSelectedItemIndex(i);
}
}}
role="option"
aria-selected={selectedItemIndex === i}
tabIndex={0}
className={`add-download-preview-row flex flex-col text-xs px-2 py-2 cursor-pointer rounded-md group ${
selectedItemIndex === i
? 'bg-blue-500/10 border border-blue-500/30 shadow-sm'
: 'hover:bg-item-hover border border-transparent'
? 'is-selected border'
: 'border border-transparent'
}`}
>
<div className="flex items-center w-full">
@@ -808,16 +832,16 @@ export const AddDownloadsModal = () => {
</div>
{/* Right Column: Settings */}
<div className="w-[45%] flex flex-col overflow-y-auto bg-bg-modal">
<div className="p-6 space-y-7">
<div className="add-download-settings w-[45%] flex flex-col overflow-y-auto">
<div className="p-6 space-y-5">
{/* Media Format (Dynamic) */}
{selectedItemIndex !== null && parsedItems[selectedItemIndex]?.isMedia && (
<section className="app-card relative overflow-hidden p-4">
<section className="add-download-section add-download-media-section relative overflow-hidden p-4">
<div className="absolute top-0 right-0 p-2 opacity-10">
<Video size={48} />
</div>
<div className="flex items-center gap-2 text-sm font-semibold text-text-primary mb-3 relative z-10">
<div className="add-download-section-title flex items-center gap-2 mb-3 relative z-10">
<Video size={16} className="text-purple-500" /> Media Format
</div>
@@ -830,25 +854,25 @@ export const AddDownloadsModal = () => {
<div className="space-y-3 relative z-10">
<div className="flex flex-col gap-1.5">
<label className="text-[10px] uppercase font-bold tracking-wider text-text-muted">Available Streams</label>
<div className="flex flex-col gap-1 max-h-48 overflow-y-auto pr-1">
<div className="flex flex-col gap-1 max-h-48 overflow-y-auto pr-1" role="radiogroup" aria-label="Available media streams">
{parsedItems[selectedItemIndex].formats!.map((f, idx) => {
const isSelected = parsedItems[selectedItemIndex].selectedFormat === idx;
const Icon = f.type === 'Audio' ? Music : Film;
return (
<div
key={idx}
onClick={() => {
const newItems = [...parsedItems];
newItems[selectedItemIndex].selectedFormat = idx;
newItems[selectedItemIndex].size = f.detail || 'Unknown';
newItems[selectedItemIndex].sizeBytes = f.bytes || 0;
// Update filename extension
const baseName = newItems[selectedItemIndex].file.substring(0, newItems[selectedItemIndex].file.lastIndexOf('.')) || newItems[selectedItemIndex].file;
newItems[selectedItemIndex].file = `${baseName}.${f.ext}`;
setParsedItems(newItems);
onClick={() => selectMediaFormat(idx)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
selectMediaFormat(idx);
}
}}
className={`flex items-center justify-between px-3 py-2 rounded-lg cursor-pointer text-xs border transition-all ${
isSelected ? 'bg-purple-500/10 border-purple-500/30 text-purple-600 dark:text-purple-400 font-semibold shadow-sm' : 'bg-bg-input border-border-modal text-text-secondary hover:border-border-modal/80 hover:bg-item-hover/50'
role="radio"
aria-checked={isSelected}
tabIndex={0}
className={`add-download-format-row flex items-center justify-between px-3 py-2 cursor-pointer text-xs border ${
isSelected ? 'is-selected text-purple-600 dark:text-purple-400 font-semibold' : 'text-text-secondary'
}`}
>
<div className="flex items-center gap-2 min-w-0">
@@ -874,8 +898,8 @@ export const AddDownloadsModal = () => {
)}
{/* Save Location */}
<section>
<div className="flex items-center gap-2 text-sm font-semibold text-text-primary mb-3">
<section className="add-download-section">
<div className="add-download-section-title flex items-center gap-2 mb-3">
<FolderPlus size={16} className="text-blue-500" /> Save Location
</div>
<div className="flex gap-2">
@@ -883,11 +907,12 @@ export const AddDownloadsModal = () => {
type="text"
readOnly
value={saveLocation}
className="flex-1 bg-bg-input border border-border-modal rounded-md px-3 py-1.5 text-xs text-text-muted font-mono"
className="add-download-control flex-1 px-3 py-1.5 text-xs text-text-muted font-mono"
aria-label="Save location"
/>
<button
onClick={handleBrowse}
className="bg-item-hover hover:bg-item-hover/80 text-text-primary border border-border-modal px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
className="add-download-button add-download-button-secondary px-3 text-xs font-medium"
>
Browse
</button>
@@ -895,14 +920,14 @@ export const AddDownloadsModal = () => {
</section>
{/* Transfer Settings */}
<section>
<div className="flex items-center gap-2 text-sm font-semibold text-text-primary mb-3">
<section className="add-download-section">
<div className="add-download-section-title flex items-center gap-2 mb-3">
<Settings size={16} className="text-blue-500" /> Transfer Settings
</div>
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<label className="text-xs text-text-secondary font-medium">Target Queue</label>
<select value={selectedQueueId} onChange={e=>setSelectedQueueId(e.target.value)} className="w-32 bg-bg-input border border-border-modal rounded-lg px-2 py-1 text-xs text-text-primary focus:border-accent focus:outline-none">
<select value={selectedQueueId} onChange={e=>setSelectedQueueId(e.target.value)} className="add-download-control add-download-select w-32 px-2 py-1 text-xs" aria-label="Target queue">
{queues.map(q => (
<option key={q.id} value={q.id}>{q.name}</option>
))}
@@ -912,19 +937,19 @@ export const AddDownloadsModal = () => {
<div className="flex items-center justify-between">
<label className="text-xs text-text-secondary font-medium">Connections per File</label>
<div className="flex items-center gap-2">
<input type="range" min="1" max="16" value={connections} onChange={e=>setConnections(Number(e.target.value))} className="w-24 accent-blue-500" disabled={parsedItems.some(i => i.isMedia)} />
<span className="text-xs text-text-primary font-mono w-4 text-right">{connections}</span>
<input type="range" min="1" max="16" value={connections} onChange={e=>setConnections(Number(e.target.value))} className="add-download-range w-24" disabled={parsedItems.some(i => i.isMedia)} aria-label="Connections per file" />
<span className="add-download-value text-xs text-text-primary font-mono w-6 text-center">{connections}</span>
</div>
</div>
<div className="flex items-center justify-between">
<label className="flex items-center gap-2 text-xs text-text-secondary font-medium cursor-pointer">
<input type="checkbox" checked={speedLimitEnabled} onChange={e=>setSpeedLimitEnabled(e.target.checked)} className="rounded border-border-modal text-blue-500 focus:ring-blue-500/20" />
<input type="checkbox" checked={speedLimitEnabled} onChange={e=>setSpeedLimitEnabled(e.target.checked)} className="add-download-checkbox" />
Limit speed per file
</label>
{speedLimitEnabled && (
<div className="flex items-center gap-1.5">
<input type="number" value={speedLimit} onChange={e=>setSpeedLimit(e.target.value)} className="w-16 bg-bg-input border border-border-modal rounded-lg px-2 py-1 text-xs font-mono text-text-primary focus:border-accent focus:outline-none" />
<input type="number" value={speedLimit} onChange={e=>setSpeedLimit(e.target.value)} className="add-download-control w-16 px-2 py-1 text-xs font-mono" aria-label="Speed limit per file" />
<span className="text-[10px] text-text-muted">KiB/s</span>
</div>
)}
@@ -933,28 +958,29 @@ export const AddDownloadsModal = () => {
</section>
{/* Authorization */}
<section>
<div className="flex items-center gap-2 text-sm font-semibold text-text-primary mb-3">
<section className="add-download-section">
<div className="add-download-section-title flex items-center gap-2 mb-3">
<Shield size={16} className="text-blue-500" /> Authorization
</div>
<label className="flex items-center gap-2 text-xs text-text-secondary font-medium cursor-pointer mb-3">
<input type="checkbox" checked={useAuth} onChange={e=>setUseAuth(e.target.checked)} className="rounded border-border-modal text-blue-500 focus:ring-blue-500/20" />
<input type="checkbox" checked={useAuth} onChange={e=>setUseAuth(e.target.checked)} className="add-download-checkbox" />
Use authorization
</label>
{useAuth && (
<div className="space-y-2.5 pl-5 border-l-2 border-border-modal/50">
<input type="text" value={username} onChange={e=>setUsername(e.target.value)} placeholder="Username" className="w-full bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs text-text-primary focus:border-accent focus:outline-none" />
<input type="password" value={password} onChange={e=>setPassword(e.target.value)} placeholder="Password" className="w-full bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs text-text-primary focus:border-accent focus:outline-none" />
<input type="text" value={username} onChange={e=>setUsername(e.target.value)} placeholder="Username" className="add-download-control w-full px-3 py-1.5 text-xs" />
<input type="password" value={password} onChange={e=>setPassword(e.target.value)} placeholder="Password" className="add-download-control w-full px-3 py-1.5 text-xs" />
</div>
)}
</section>
{/* Advanced */}
<section className="pt-2 border-t border-border-modal/50">
<section className="add-download-section add-download-advanced">
<button
onClick={() => setAdvancedExpanded(!advancedExpanded)}
className="flex items-center gap-2 text-sm font-semibold text-text-primary w-full hover:text-blue-500 transition-colors"
className="add-download-advanced-toggle flex items-center gap-2 text-sm font-semibold text-text-primary w-full"
aria-expanded={advancedExpanded}
>
{advancedExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
Advanced Transfer
@@ -963,30 +989,30 @@ export const AddDownloadsModal = () => {
{advancedExpanded && (
<div className="mt-4 space-y-4 pl-6">
<label className="flex items-center gap-2 text-xs text-text-secondary font-medium cursor-pointer">
<input type="checkbox" checked={checksumEnabled} onChange={e=>setChecksumEnabled(e.target.checked)} className="rounded border-border-modal text-blue-500 focus:ring-blue-500/20" />
<input type="checkbox" checked={checksumEnabled} onChange={e=>setChecksumEnabled(e.target.checked)} className="add-download-checkbox" />
Verify Checksum
</label>
{checksumEnabled && (
<div className="flex gap-2">
<select value={checksumAlgo} onChange={e=>setChecksumAlgo(e.target.value)} className="w-24 bg-bg-input border border-border-modal rounded-lg px-2 text-xs text-text-primary focus:border-accent focus:outline-none">
<select value={checksumAlgo} onChange={e=>setChecksumAlgo(e.target.value)} className="add-download-control add-download-select w-24 px-2 text-xs" aria-label="Checksum algorithm">
<option>MD5</option><option>SHA-1</option><option>SHA-256</option>
</select>
<input type="text" value={checksumValue} onChange={e=>setChecksumValue(e.target.value)} placeholder="Expected digest" className="flex-1 bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs font-mono text-text-primary focus:border-accent focus:outline-none" />
<input type="text" value={checksumValue} onChange={e=>setChecksumValue(e.target.value)} placeholder="Expected digest" className="add-download-control flex-1 px-3 py-1.5 text-xs font-mono" />
</div>
)}
<div>
<label className="block text-[10px] uppercase font-bold tracking-wider text-text-muted mb-1">Headers</label>
<textarea value={headers} onChange={e=>setHeaders(e.target.value)} className="w-full h-12 bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs font-mono text-text-primary focus:border-accent focus:outline-none resize-none" />
<textarea value={headers} onChange={e=>setHeaders(e.target.value)} className="add-download-control w-full h-12 px-3 py-1.5 text-xs font-mono resize-none" aria-label="Request headers" />
</div>
<div>
<label className="block text-[10px] uppercase font-bold tracking-wider text-text-muted mb-1">Cookies</label>
<input type="text" value={cookies} onChange={e=>setCookies(e.target.value)} placeholder="name=value; other=value" className="w-full bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs font-mono text-text-primary focus:border-accent focus:outline-none" />
<input type="text" value={cookies} onChange={e=>setCookies(e.target.value)} placeholder="name=value; other=value" className="add-download-control w-full px-3 py-1.5 text-xs font-mono" aria-label="Cookies" />
</div>
<div>
<label className="block text-[10px] uppercase font-bold tracking-wider text-text-muted mb-1">Mirrors</label>
<textarea value={mirrors} onChange={e=>setMirrors(e.target.value)} className="w-full h-12 bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs font-mono text-text-primary focus:border-accent focus:outline-none resize-none" />
<textarea value={mirrors} onChange={e=>setMirrors(e.target.value)} className="add-download-control w-full h-12 px-3 py-1.5 text-xs font-mono resize-none" aria-label="Mirrors" />
</div>
</div>
)}
@@ -997,25 +1023,25 @@ export const AddDownloadsModal = () => {
</div>
{/* Footer */}
<div className="p-4 bg-sidebar-bg/50 border-t border-border-modal flex items-center shrink-0">
<div className="add-download-footer p-4 flex items-center shrink-0">
<div className="text-[11px] text-text-muted font-medium flex-1">
{parsedItems.length === 0 ? "Paste one or more links." : `Ready to add ${parsedItems.length} download(s).`}
</div>
<div className="flex gap-2.5">
<button onClick={() => toggleAddModal(false)} className="app-button border-transparent bg-transparent px-4 text-xs text-text-secondary">
<button onClick={() => toggleAddModal(false)} className="add-download-button add-download-button-cancel px-4 text-xs">
Cancel
</button>
<button
onClick={() => handleStart(false)}
disabled={parsedItems.length === 0}
className="app-button px-4 text-xs disabled:opacity-50"
className="add-download-button add-download-button-secondary px-4 text-xs"
>
Add to Queue
</button>
<button
onClick={() => handleStart(true)}
disabled={parsedItems.length === 0}
className="app-button app-button-primary px-5 text-xs disabled:opacity-50"
className="add-download-button add-download-button-primary px-5 text-xs"
>
<Play size={12} fill="currentColor" /> Start Downloads
</button>