feat: refine sidebar UI and update download categories

- Restructured layout so the sidebar acts as an edge-to-edge curved layer with a shadow
- Replaced old download categories with Musics, Movies, Compressed, Documents, Pictures, Applications, and Other
- Updated Settings > Locations tab to handle the new categories
- Added extensive predefined file extensions for smart auto-categorization of downloads
This commit is contained in:
NimBold
2026-06-13 00:02:07 +03:30
parent 610eda203e
commit dc9289fece
10 changed files with 137 additions and 48 deletions
+32 -7
View File
@@ -27,9 +27,11 @@ function App() {
const activeView = useSettingsStore(state => state.activeView);
const appFontSize = useSettingsStore(state => state.appFontSize);
const showDockBadge = useSettingsStore(state => state.showDockBadge);
const activeDownloadCount = useDownloadStore(state => state.downloads.filter(download => download.status === 'downloading').length);
const schedulerRunning = useSettingsStore(state => state.schedulerRunning);
const downloads = useDownloadStore(state => state.downloads);
const activeDownloadCount = downloads.filter(download => download.status === 'downloading').length;
const queuedCount = downloads.filter(download => download.status === 'queued').length;
const doneCount = downloads.filter(download => download.status === 'completed').length;
const schedulerRunning = useSettingsStore(state => state.schedulerRunning);
const globalSpeedLimit = useSettingsStore(state => state.globalSpeedLimit);
const previousSpeedLimit = useRef(globalSpeedLimit);
@@ -179,11 +181,34 @@ function App() {
return (
<div className="flex h-screen w-screen bg-main-bg text-text-primary overflow-hidden">
{isSidebarVisible && <Sidebar selectedFilter={filter} onSelectFilter={(f) => { setFilter(f); useSettingsStore.getState().setActiveView('downloads'); }} />}
{activeView === 'downloads' && <DownloadTable filter={filter} />}
{activeView === 'settings' && <SettingsView />}
{activeView === 'scheduler' && <SchedulerView />}
{activeView === 'speedLimiter' && <SpeedLimiterView />}
{/* Left Side Panel - Curved Second Layer on Top */}
{isSidebarVisible && (
<div className="w-[240px] bg-sidebar-bg rounded-r-[16px] shadow-[4px_0_24px_rgba(0,0,0,0.3)] border-r border-border-color/20 flex flex-col overflow-hidden relative z-20 shrink-0">
<Sidebar selectedFilter={filter} onSelectFilter={(f) => { setFilter(f); useSettingsStore.getState().setActiveView('downloads'); }} />
</div>
)}
{/* Main Content - Base Layer */}
<div className="flex-1 flex flex-col h-full relative z-0">
<div className="flex-1 flex flex-col overflow-hidden relative">
{activeView === 'downloads' && <DownloadTable filter={filter} />}
{activeView === 'settings' && <SettingsView />}
{activeView === 'scheduler' && <SchedulerView />}
{activeView === 'speedLimiter' && <SpeedLimiterView />}
</div>
{/* Status Bar */}
<div className="h-7 px-5 flex items-center justify-between text-[11px] text-text-muted font-medium shrink-0 border-t border-border-color/30">
<span>Ready</span>
<div className="flex gap-2">
<span>{activeDownloadCount} active</span>
<span>{queuedCount} queued</span>
<span>{doneCount} done</span>
</div>
</div>
</div>
<AddDownloadsModal />
<PropertiesModal />
</div>
@@ -8,12 +8,12 @@ import { invoke } from '@tauri-apps/api/core';
function determineCategory(fileName: string): DownloadCategory {
const ext = fileName.split('.').pop()?.toLowerCase() || '';
if (['mp4', 'mkv', 'webm', 'avi', 'mov', 'flv'].includes(ext)) return 'Video';
if (['mp3', 'm4a', 'wav', 'flac', 'ogg', 'aac'].includes(ext)) return 'Audio';
if (['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'm4v'].includes(ext)) return 'Movies';
if (['mp3', 'wav', 'aac', 'flac', 'ogg', 'm4a', 'wma'].includes(ext)) return 'Musics';
if (['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'rtf'].includes(ext)) return 'Documents';
if (['exe', 'dmg', 'pkg', 'app', 'apk', 'deb', 'rpm'].includes(ext)) return 'Apps';
if (['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'tiff'].includes(ext)) return 'Images';
if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz'].includes(ext)) return 'Archives';
if (['exe', 'dmg', 'apk', 'app', 'pkg', 'deb', 'rpm', 'msi', 'iso', 'bin', 'run'].includes(ext)) return 'Applications';
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff', 'svg'].includes(ext)) return 'Pictures';
if (['zip', 'rar', '7z', 'tar', 'gz', 'xz', 'bz2'].includes(ext)) return 'Compressed';
return 'Other';
}
+8 -12
View File
@@ -99,18 +99,19 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
const getCategoryIcon = (category: string) => {
switch(category) {
case 'Musics': return <Music size={16} className="text-pink-400" />;
case 'Movies': return <Film size={16} className="text-red-400" />;
case 'Documents': return <FileText size={16} className="text-blue-400" />;
case 'Images': return <ImageIcon size={16} className="text-purple-400" />;
case 'Audio': return <Music size={16} className="text-pink-400" />;
case 'Video': return <Film size={16} className="text-red-400" />;
case 'Apps': return <Box size={16} className="text-orange-400" />;
case 'Archives': return <Archive size={16} className="text-yellow-400" />;
case 'Applications': return <Box size={16} className="text-indigo-400" />;
case 'Pictures': return <ImageIcon size={16} className="text-purple-400" />;
case 'Compressed': return <Archive size={16} className="text-amber-600" />;
case 'Other': return <FileQuestion size={16} className="text-gray-400" />;
default: return <FileQuestion size={16} className="text-gray-400" />;
}
}
return (
<div className="flex-1 flex flex-col bg-main-bg h-full relative">
<div className="flex-1 flex flex-col bg-transparent h-full relative">
<div className="glass-panel shrink-0 border-b border-border-color/60">
<WindowDragRegion className={!isSidebarVisible ? 'pl-24' : ''} />
@@ -163,7 +164,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
</div>
{/* Table */}
<div className="flex-1 overflow-auto bg-main-bg">
<div className="flex-1 overflow-auto bg-transparent">
<table className="w-full border-collapse text-left">
<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">
@@ -269,11 +270,6 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
</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' : ''}
</div>
{/* Floating Context Menu */}
{contextMenu && contextItem && (
<div
+11 -12
View File
@@ -95,21 +95,20 @@ export default function SettingsView() {
const handleBrowseBulk = async () => {
try {
const selected = await open({
const base = await open({
directory: true,
multiple: false
});
if (selected && typeof selected === 'string') {
// Automatically populate all category folders
const cleanBase = selected.endsWith('/') ? selected.slice(0, -1) : selected;
settings.setCategoryDirectory('Video', `${cleanBase}/Video`);
settings.setCategoryDirectory('Audio', `${cleanBase}/Audio`);
if (base && typeof base === 'string') {
const cleanBase = base.replace(/\/$/, '');
settings.setCategoryDirectory('Musics', `${cleanBase}/Musics`);
settings.setCategoryDirectory('Movies', `${cleanBase}/Movies`);
settings.setCategoryDirectory('Compressed', `${cleanBase}/Compressed`);
settings.setCategoryDirectory('Documents', `${cleanBase}/Documents`);
settings.setCategoryDirectory('Apps', `${cleanBase}/Apps`);
settings.setCategoryDirectory('Images', `${cleanBase}/Images`);
settings.setCategoryDirectory('Archives', `${cleanBase}/Archives`);
settings.setCategoryDirectory('Pictures', `${cleanBase}/Pictures`);
settings.setCategoryDirectory('Applications', `${cleanBase}/Applications`);
settings.setCategoryDirectory('Other', `${cleanBase}/Other`);
showToast("Created subfolders for all categories");
showToast("Updated all categories to use base folder");
}
} catch (e) {
console.error("Failed to browse base path:", e);
@@ -492,13 +491,13 @@ export default function SettingsView() {
</div>
</div>
{Object.keys(settings.downloadDirectories || {}).map((category) => (
{['Musics', 'Movies', 'Compressed', 'Documents', 'Pictures', 'Applications', 'Other'].map((category) => (
<div key={category} className="grid grid-cols-[150px_1fr] items-center gap-4 text-[13px]">
<label className="text-text-secondary capitalize">{category} folder:</label>
<div className="flex gap-2">
<input
type="text"
value={(settings.downloadDirectories || {})[category]}
value={(settings.downloadDirectories || {})[category] || ''}
onChange={(e) => settings.setCategoryDirectory(category, e.target.value)}
className="flex-1 bg-bg-input border border-border-modal rounded-md px-3 py-1 text-xs text-text-primary font-mono"
/>
+6 -6
View File
@@ -168,7 +168,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
};
return (
<aside className="w-[220px] min-w-[190px] max-w-[260px] bg-sidebar-bg border-r border-border-color flex flex-col relative shrink-0">
<aside className="w-full h-full flex flex-col relative shrink-0">
<WindowDragRegion />
<div className="overflow-y-auto flex-1 px-2 pb-3">
<section className="mb-4">
@@ -181,12 +181,12 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
<section className="mb-4">
<div className="text-[11px] font-semibold text-text-muted px-2.5 mb-1.5">Folders</div>
<NavItem icon={Film} label="Video" filter="Video" />
<NavItem icon={Music} label="Audio" filter="Audio" />
<NavItem icon={Music} label="Musics" filter="Musics" />
<NavItem icon={Film} label="Movies" filter="Movies" />
<NavItem icon={Archive} label="Compressed" filter="Compressed" />
<NavItem icon={FileText} label="Documents" filter="Documents" />
<NavItem icon={Box} label="Apps" filter="Apps" />
<NavItem icon={ImageIcon} label="Images" filter="Images" />
<NavItem icon={Archive} label="Archives" filter="Archives" />
<NavItem icon={ImageIcon} label="Pictures" filter="Pictures" />
<NavItem icon={Box} label="Applications" filter="Applications" />
<NavItem icon={FileQuestion} label="Other" filter="Other" />
</section>
+1 -1
View File
@@ -77,7 +77,7 @@ const effectiveSpeedLimit = (
};
export type DownloadStatus = 'downloading' | 'paused' | 'completed' | 'failed' | 'queued';
export type DownloadCategory = 'Documents' | 'Images' | 'Audio' | 'Video' | 'Apps' | 'Archives' | 'Other';
export type DownloadCategory = 'Musics' | 'Movies' | 'Compressed' | 'Documents' | 'Pictures' | 'Applications' | 'Other';
export const MAIN_QUEUE_ID = '00000000-0000-0000-0000-000000000001';
+5 -5
View File
@@ -93,12 +93,12 @@ export interface SettingsState {
}
const defaultDirectories = {
Video: '~/Downloads/Video',
Audio: '~/Downloads/Audio',
Musics: '~/Downloads/Musics',
Movies: '~/Downloads/Movies',
Compressed: '~/Downloads/Compressed',
Documents: '~/Downloads/Documents',
Apps: '~/Downloads/Apps',
Images: '~/Downloads/Images',
Archives: '~/Downloads/Compressed',
Pictures: '~/Downloads/Pictures',
Applications: '~/Downloads/Applications',
Other: '~/Downloads/Other'
};
+2
View File
@@ -0,0 +1,2 @@
import { invoke } from '@tauri-apps/api/core';
invoke('start_media_download', { id: "test", url: "https://www.youtube.com/watch?v=1PBRhm5ZnjU", destination: "/tmp", filename: "test.mp4", formatSelector: null }).then(console.log).catch(console.error);
+13
View File
@@ -0,0 +1,13 @@
use regex::Regex;
fn main() {
let pct_re = Regex::new(r"\[download\]\s+(\d+(?:\.\d+)?)%").unwrap();
let line = "[download] 0.0% of 15.59MiB at 7.28KiB/s ETA 36:34";
if line.contains("[download]") && line.contains("%") {
let fraction = pct_re.captures(&line)
.and_then(|cap| cap.get(1))
.and_then(|m| m.as_str().parse::<f64>().ok())
.unwrap_or(0.0) / 100.0;
println!("Fraction: {}", fraction);
}
}
+54
View File
@@ -0,0 +1,54 @@
[youtube] Extracting URL: https://www.youtube.com/watch?v=1PBRhm5ZnjU
[youtube] 1PBRhm5ZnjU: Downloading webpage
[youtube] 1PBRhm5ZnjU: Downloading android vr player API JSON
[youtube] 1PBRhm5ZnjU: Downloading player 445213fb-main
[youtube] [jsc:deno] Solving JS challenges using deno
[youtube] 1PBRhm5ZnjU: Downloading m3u8 information
[info] 1PBRhm5ZnjU: Downloading 1 format(s): 399+140
[download] Destination: /Users/nima/Downloads/test2.f399.mp4
[download] 0.0% of 15.59MiB at 7.28KiB/s ETA 36:34
[download] 0.0% of 15.59MiB at 21.51KiB/s ETA 12:22
[download] 0.0% of 15.59MiB at 49.76KiB/s ETA 05:20
[download] 0.1% of 15.59MiB at 106.20KiB/s ETA 02:30
[download] 0.2% of 15.59MiB at 152.25KiB/s ETA 01:44
[download] 0.4% of 15.59MiB at 214.52KiB/s ETA 01:14
[download] 0.8% of 15.59MiB at 330.14KiB/s ETA 00:47
[download] 1.6% of 15.59MiB at 534.67KiB/s ETA 00:29
[download] 3.2% of 15.59MiB at 765.21KiB/s ETA 00:20
[download] 6.4% of 15.59MiB at 1.23MiB/s ETA 00:11
[download] 12.8% of 15.59MiB at 1.97MiB/s ETA 00:06
[download] 25.6% of 15.59MiB at 3.05MiB/s ETA 00:03
[download] 51.3% of 15.59MiB at 3.56MiB/s ETA 00:02
[download] 61.6% of 15.59MiB at 3.72MiB/s ETA 00:01
[download] 61.6% of 15.59MiB at 43.49KiB/s ETA 02:20
[download] 61.6% of 15.59MiB at 113.07KiB/s ETA 00:54
[download] 61.7% of 15.59MiB at 254.33KiB/s ETA 00:24
[download] 61.7% of 15.59MiB at 532.02KiB/s ETA 00:11
[download] 61.8% of 15.59MiB at 1.05MiB/s ETA 00:05
[download] 62.0% of 15.59MiB at 2.10MiB/s ETA 00:02
[download] 62.4% of 15.59MiB at 3.06MiB/s ETA 00:01
[download] 63.2% of 15.59MiB at 3.17MiB/s ETA 00:01
[download] 64.8% of 15.59MiB at 3.10MiB/s ETA 00:01
[download] 68.0% of 15.59MiB at 3.04MiB/s ETA 00:01
[download] 74.4% of 15.59MiB at 2.70MiB/s ETA 00:01
[download] 87.3% of 15.59MiB at 2.60MiB/s ETA 00:00
[download] 100.0% of 15.59MiB at 2.72MiB/s ETA 00:00
[download] 100% of 15.59MiB in 00:00:08 at 1.91MiB/s
[download] Destination: /Users/nima/Downloads/test2.f140.m4a
[download] 0.0% of 4.76MiB at 837.69KiB/s ETA 00:05
[download] 0.1% of 4.76MiB at 1.54MiB/s ETA 00:03
[download] 0.1% of 4.76MiB at 2.40MiB/s ETA 00:01
[download] 0.3% of 4.76MiB at 4.34MiB/s ETA 00:01
[download] 0.6% of 4.76MiB at 7.93MiB/s ETA 00:00
[download] 1.3% of 4.76MiB at 14.52MiB/s ETA 00:00
[download] 2.6% of 4.76MiB at 26.50MiB/s ETA 00:00
[download] 5.2% of 4.76MiB at 7.70MiB/s ETA 00:00
[download] 10.5% of 4.76MiB at 5.40MiB/s ETA 00:00
[download] 21.0% of 4.76MiB at 4.61MiB/s ETA 00:00
[download] 42.0% of 4.76MiB at 4.31MiB/s ETA 00:00
[download] 84.0% of 4.76MiB at 4.15MiB/s ETA 00:00
[download] 100.0% of 4.76MiB at 4.11MiB/s ETA 00:00
[download] 100% of 4.76MiB in 00:00:01 at 3.20MiB/s
[Merger] Merging formats into "/Users/nima/Downloads/test2.mp4"
Deleting original file /Users/nima/Downloads/test2.f140.m4a (pass -k to keep)
Deleting original file /Users/nima/Downloads/test2.f399.mp4 (pass -k to keep)