feat(desktop): modernize download management

This commit is contained in:
NimBold
2026-06-12 20:33:17 +03:30
parent 3074438147
commit a5022fbf8a
9 changed files with 492 additions and 383 deletions
+104 -95
View File
@@ -103,74 +103,72 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
return (
<div className="flex-1 flex flex-col bg-main-bg h-full relative">
{/* Modern Toolbar */}
<div
className={`flex px-6 py-4 border-b border-border-color items-center glass-panel z-10 sticky top-0 ${!isSidebarVisible ? 'pl-24' : ''}`}
<div
className={`flex px-6 py-4 items-center glass-panel z-10 sticky top-0 ${!isSidebarVisible ? 'pl-24' : ''}`}
onPointerDown={(e) => {
if (e.button === 0 && (e.target as HTMLElement).closest('.no-drag') === null) {
getCurrentWindow().startDragging();
}
}}
>
<button
<button
onClick={toggleSidebar}
className="no-drag mr-3 p-1.5 rounded-lg text-text-secondary hover:text-text-primary hover:bg-item-hover transition-colors"
className="no-drag mr-4 p-1.5 rounded-md text-text-secondary hover:text-text-primary hover:bg-item-hover transition-colors"
title="Toggle Sidebar"
>
<PanelLeft size={18} strokeWidth={2} />
</button>
<h2 className="text-lg font-bold mr-auto text-text-primary tracking-tight cursor-default">{getFilterTitle()}</h2>
<div className="flex gap-1.5 items-center bg-bg-input/50 p-1 rounded-xl border border-border-modal/50 shadow-sm no-drag">
<button
<h2 className="text-[15px] font-bold mr-auto text-text-primary tracking-tight cursor-default">{getFilterTitle()}</h2>
<div className="flex gap-1.5 items-center no-drag">
<button
onClick={() => toggleAddModal(true)}
className="p-2 rounded-lg text-text-secondary hover:text-blue-500 hover:bg-blue-500/10 transition-all duration-200 group relative"
className="p-1.5 rounded-md text-text-secondary hover:text-white hover:bg-item-hover transition-all duration-200"
title="Add Download"
>
<Plus size={18} strokeWidth={2.5} />
<Plus size={16} strokeWidth={2} />
</button>
<div className="w-[1px] h-4 bg-border-color/60 mx-1"></div>
<button
<button
onClick={() => {
filteredDownloads.filter(d => d.status === 'paused').forEach(d => handleResume(d));
}}
className="p-2 rounded-lg text-text-secondary hover:text-green-500 hover:bg-green-500/10 transition-all duration-200"
className="p-1.5 rounded-md text-text-secondary hover:text-white hover:bg-item-hover transition-all duration-200"
title="Resume All"
>
<Play size={18} fill="currentColor" className="opacity-80" />
<Play size={16} fill="currentColor" className="opacity-90" />
</button>
<button
<button
onClick={() => {
filteredDownloads.filter(d => d.status === 'downloading').forEach(d => handlePause(d.id));
}}
className="p-2 rounded-lg text-text-secondary hover:text-orange-500 hover:bg-orange-500/10 transition-all duration-200"
className="p-1.5 rounded-md text-text-secondary hover:text-white hover:bg-item-hover transition-all duration-200"
title="Pause All"
>
<Pause size={18} fill="currentColor" className="opacity-80" />
<Pause size={16} fill="currentColor" className="opacity-90" />
</button>
<div className="w-[1px] h-4 bg-border-color/60 mx-1"></div>
<button
<button
onClick={clearFinished}
className="p-2 rounded-lg text-text-secondary hover:text-red-500 hover:bg-red-500/10 transition-all duration-200"
className="p-1.5 rounded-md text-text-secondary hover:text-red-400 hover:bg-item-hover transition-all duration-200"
title="Clear Finished"
>
<Trash2 size={18} />
<Trash2 size={16} />
</button>
</div>
</div>
{/* Table */}
<div className="flex-1 overflow-auto">
<div className="flex-1 overflow-auto bg-main-bg">
<table className="w-full border-collapse text-left">
<thead className="sticky top-0 bg-main-bg/95 backdrop-blur-md z-0 shadow-sm border-b border-border-color">
<tr className="text-text-muted text-xs uppercase tracking-wider font-semibold">
<th className={`${py} px-3 pl-6`}>File</th>
<th className={`${py} px-3`}>Size</th>
<th className={`${py} px-3`}>Status</th>
<th className={`${py} px-3`}>Speed</th>
<th className={`${py} px-3 pr-6`}>ETA</th>
<th className={`${py} px-3 w-16`}></th>
<thead className="sticky top-0 bg-main-bg z-0 border-b border-border-color">
<tr className="text-text-muted/60 text-[10px] font-bold tracking-widest uppercase">
<th className={`${py} px-3 pl-8`}>FILE</th>
<th className={`${py} px-3 w-40`}>SIZE</th>
<th className={`${py} px-3 w-36`}>STATUS</th>
<th className={`${py} px-3 w-28`}>SPEED</th>
<th className={`${py} px-3 w-28`}>ETA</th>
<th className={`${py} px-3 pr-8 w-36`}>DATE ADDED</th>
</tr>
</thead>
<tbody>
@@ -185,8 +183,8 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
</tr>
) : (
filteredDownloads.map(d => (
<tr
key={d.id}
<tr
key={d.id}
className="border-b border-border-color/30 hover:bg-item-hover transition-colors duration-200 group cursor-default"
onContextMenu={(e) => {
e.preventDefault();
@@ -197,56 +195,67 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
});
}}
>
<td className={`${py} px-3 pl-6 text-sm text-text-primary`}>
<td className={`${py} px-3 pl-8 text-[13px] text-text-primary`}>
<div className="flex items-center gap-3">
<div className="p-2 bg-bg-input/50 rounded-lg shadow-sm border border-border-modal/20">
<div className="p-2 bg-item-hover rounded-md border border-border-color/50 text-text-muted">
{getCategoryIcon(d.category)}
</div>
<span className="font-medium truncate max-w-[250px]">{d.fileName}</span>
<span className="font-medium truncate max-w-[280px]">{d.fileName}</span>
</div>
</td>
<td className={`${py} px-3 text-[13px] text-text-secondary w-32`}>
<div className="w-full bg-border-color rounded-full h-1.5 mb-1.5 mt-0.5 overflow-hidden shadow-inner">
<div className={`h-1.5 rounded-full transition-all duration-300 ${d.status === 'completed' ? 'bg-green-500' : d.status === 'paused' ? 'bg-orange-500' : d.status === 'failed' ? 'bg-red-500' : 'bg-blue-500'}`} style={{ width: `${(d.fraction || 0) * 100}%` }}></div>
</div>
<div className="text-[11px] font-mono text-text-muted font-medium">
{((d.fraction || 0) * 100).toFixed(1)}%
</div>
<td className={`${py} px-3`}>
{d.status === 'downloading' || d.status === 'paused' ? (
<div className="w-full pr-4">
<div className="w-full bg-[#3f3f3f] rounded-full h-1.5 mb-1.5 overflow-hidden">
<div className={`h-1.5 rounded-full transition-all duration-300 ${d.status === 'paused' ? 'bg-orange-500' : 'bg-[#3B66DE]'}`} style={{ width: `${(d.fraction || 0) * 100}%` }}></div>
</div>
<div className="text-[11px] text-text-muted font-medium">
{((d.fraction || 0) * 100).toFixed(1)}%
</div>
</div>
) : (
<span className="text-[12px] text-text-secondary font-medium">{d.size || '-'}</span>
)}
</td>
<td className={`${py} px-3 text-[13px]`}>
<span className={`inline-flex items-center px-2 py-0.5 rounded-md text-[11px] font-bold uppercase tracking-wider ${
d.status === 'completed' ? 'bg-green-500/10 text-green-500' :
d.status === 'downloading' ? 'bg-blue-500/10 text-blue-500' :
d.status === 'failed' ? 'bg-red-500/10 text-red-500' :
d.status === 'paused' ? 'bg-orange-500/10 text-orange-500' :
'bg-zinc-500/10 text-text-secondary'
<td className={`${py} px-3`}>
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold tracking-widest uppercase ${
d.status === 'completed' ? 'text-[#4CAF50]' :
d.status === 'downloading' ? 'bg-[#1E3A8A] text-[#60A5FA]' :
d.status === 'failed' ? 'text-red-400' :
d.status === 'paused' ? 'text-orange-400' :
'text-text-muted'
}`}>
{d.status}
</span>
</td>
<td className={`${py} px-3 text-[12px] text-text-secondary font-mono`}>{d.speed}</td>
<td className={`${py} px-3 pr-6 text-[12px] text-text-secondary font-mono`}>{d.eta}</td>
<td className={`${py} px-3 pr-6 text-right opacity-0 group-hover:opacity-100 transition-opacity duration-200`}>
<div className="flex justify-end gap-1.5">
{d.status === 'downloading' && (
<button onClick={() => handlePause(d.id)} className="p-1.5 bg-bg-input/80 shadow-sm border border-border-modal/50 hover:bg-item-hover rounded-md text-text-muted hover:text-orange-500 transition-colors" title="Pause">
<Pause size={14} fill="currentColor" />
<td className={`${py} px-3 text-[12px] text-text-secondary font-medium`}>{d.speed}</td>
<td className={`${py} px-3 text-[12px] text-text-secondary font-medium`}>{d.eta}</td>
<td className={`${py} px-3 pr-8 relative`}>
<div className="flex items-center justify-between">
<span className="text-[12px] text-text-secondary font-medium">
{d.dateAdded ? new Date(d.dateAdded).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) : '-'}
</span>
<div className="flex justify-end gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity duration-200 absolute right-8 top-1/2 -translate-y-1/2">
{d.status === 'downloading' && (
<button onClick={() => handlePause(d.id)} className="p-1 bg-[#2A2B2D] border border-[#3f3f3f] hover:bg-[#3f3f3f] rounded text-orange-400 transition-colors" title="Pause">
<Pause size={14} fill="currentColor" />
</button>
)}
{d.status === 'paused' && (
<button onClick={() => handleResume(d)} className="p-1 bg-[#2A2B2D] border border-[#3f3f3f] hover:bg-[#3f3f3f] rounded text-green-400 transition-colors" title="Resume">
<Play size={14} fill="currentColor" />
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
setContextMenu({ x: e.clientX, y: e.clientY, id: d.id });
}}
className="p-1 bg-[#2A2B2D] border border-[#3f3f3f] hover:bg-[#3f3f3f] rounded text-text-muted hover:text-text-primary transition-colors"
>
<MoreVertical size={14} />
</button>
)}
{d.status === 'paused' && (
<button onClick={() => handleResume(d)} className="p-1.5 bg-bg-input/80 shadow-sm border border-border-modal/50 hover:bg-item-hover rounded-md text-text-muted hover:text-green-500 transition-colors" title="Resume">
<Play size={14} fill="currentColor" />
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
setContextMenu({ x: e.clientX, y: e.clientY, id: d.id });
}}
className="p-1.5 bg-bg-input/80 shadow-sm border border-border-modal/50 hover:bg-item-hover rounded-md text-text-muted hover:text-text-primary transition-colors"
>
<MoreVertical size={14} />
</button>
</div>
</div>
</td>
</tr>
@@ -255,7 +264,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
</tbody>
</table>
</div>
{/* Status Bar */}
<div className="px-6 py-2 border-t border-border-color text-[11px] font-medium text-text-muted bg-sidebar-bg/50 backdrop-blur-md">
{downloads.length} Item{downloads.length !== 1 ? 's' : ''}
@@ -263,16 +272,16 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
{/* Floating Context Menu */}
{contextMenu && contextItem && (
<div
<div
className="fixed z-50 bg-bg-modal/95 backdrop-blur-xl border border-border-modal rounded-xl shadow-2xl py-1.5 min-w-[180px] text-[13px] font-medium text-text-primary overflow-hidden"
style={{
top: Math.min(contextMenu.y, window.innerHeight - 300),
left: Math.min(contextMenu.x, window.innerWidth - 200)
style={{
top: Math.min(contextMenu.y, window.innerHeight - 300),
left: Math.min(contextMenu.x, window.innerWidth - 200)
}}
onClick={(e) => e.stopPropagation()}
>
{contextItem.status === 'completed' && (
<button
<button
onClick={async () => {
setContextMenu(null);
try {
@@ -281,14 +290,14 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
} catch (e) {
console.error("Failed to open file:", e);
}
}}
}}
className="w-full text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors"
>
Open File
</button>
)}
<button
<button
onClick={async () => {
setContextMenu(null);
try {
@@ -297,7 +306,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
} catch (e) {
console.error("Failed to show in folder:", e);
}
}}
}}
className="w-full text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors"
>
Show in Finder
@@ -306,11 +315,11 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
{(contextItem.status === 'downloading' || contextItem.status === 'queued') && (
<button
<button
onClick={() => {
setContextMenu(null);
handlePause(contextItem.id);
}}
}}
className="w-full text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors"
>
Pause
@@ -318,11 +327,11 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
)}
{(contextItem.status === 'paused' || contextItem.status === 'failed') && (
<button
<button
onClick={() => {
setContextMenu(null);
handleResume(contextItem);
}}
}}
className="w-full text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors"
>
Resume
@@ -330,11 +339,11 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
)}
{['completed', 'failed', 'paused'].includes(contextItem.status) && (
<button
<button
onClick={() => {
setContextMenu(null);
redownload(contextItem.id);
}}
}}
className="w-full text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors"
>
Redownload
@@ -343,23 +352,23 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
<button
<button
onClick={() => {
setContextMenu(null);
navigator.clipboard.writeText(contextItem.url);
}}
}}
className="w-full text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors"
>
Copy Address
</button>
{contextItem.status === 'completed' && (
<button
<button
onClick={async () => {
setContextMenu(null);
const fullPath = await resolvePath(contextItem.destination || '~/Downloads', contextItem.fileName);
navigator.clipboard.writeText(fullPath);
}}
}}
className="w-full text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors"
>
Copy File Path
@@ -368,11 +377,11 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
<button
<button
onClick={() => {
setContextMenu(null);
handleDelete(contextItem.id);
}}
}}
className="w-full text-left px-4 py-1.5 hover:bg-red-500 hover:text-white text-red-500 transition-colors"
>
Remove from List
@@ -380,11 +389,11 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
<button
<button
onClick={() => {
setContextMenu(null);
useDownloadStore.getState().setSelectedPropertiesDownloadId(contextItem.id);
}}
}}
className="w-full text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors"
>
Properties