mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 20:18:37 +00:00
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:
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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
@@ -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)
|
||||
Reference in New Issue
Block a user