style(ui): redesign app layout to native macOS density and refine visual theme

- Adjusted overall layout scale to perfectly match original native macOS density.

- Refined sidebar into a floating rounded slab with proper padding, border, and toggle button placement.

- Fixed drag region overlap preventing button interactions in the title bar.

- Removed artificial shadow lines and scrollbars from the sidebar.

- Updated Tauri backend configuration, dependencies, and scheduler functions for improved stability.
This commit is contained in:
NimBold
2026-06-14 08:36:53 +03:30
parent 4ec1c4fdf6
commit 796d00d527
12 changed files with 941 additions and 597 deletions
+4 -1
View File
@@ -4,7 +4,10 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + React + Typescript</title> <title>Firelink Download Manager</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head> </head>
<body> <body>
+17 -1
View File
@@ -4402,7 +4402,7 @@ dependencies = [
"url", "url",
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
"window-vibrancy", "window-vibrancy 0.6.0",
"windows 0.61.3", "windows 0.61.3",
] ]
@@ -4440,6 +4440,7 @@ dependencies = [
"tokio", "tokio",
"tokio-tungstenite", "tokio-tungstenite",
"tower-http", "tower-http",
"window-vibrancy 0.7.1",
] ]
[[package]] [[package]]
@@ -5683,6 +5684,21 @@ dependencies = [
"windows-version", "windows-version",
] ]
[[package]]
name = "window-vibrancy"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "010797bd7c40396fbc59d3105089fed0885fe267a0ef4a0a4646df54e28647f6"
dependencies = [
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"raw-window-handle",
"windows-sys 0.60.2",
"windows-version",
]
[[package]] [[package]]
name = "windows" name = "windows"
version = "0.61.3" version = "0.61.3"
+2 -1
View File
@@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
tauri = { version = "2", features = ["tray-icon", "image-png"] } tauri = { version = "2", features = ["macos-private-api", "tray-icon", "image-png"] }
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
tauri-plugin-dialog = "2" tauri-plugin-dialog = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
@@ -47,3 +47,4 @@ rusqlite = { version = "0.40.1", features = ["bundled"] }
chrono = "0.4.38" chrono = "0.4.38"
cocoa = "0.25" cocoa = "0.25"
objc = "0.2.7" objc = "0.2.7"
window-vibrancy = "0.7.1"
+12 -2
View File
@@ -714,7 +714,7 @@ pub(crate) async fn start_media_download_internal(
let spd_re = Regex::new(r"at\s+([^\s]+)").unwrap(); let spd_re = Regex::new(r"at\s+([^\s]+)").unwrap();
let eta_re = Regex::new(r"ETA\s+([^\s]+)").unwrap(); let eta_re = Regex::new(r"ETA\s+([^\s]+)").unwrap();
tokio::spawn(async move { tauri::async_runtime::spawn(async move {
let _keep_alive = config_path; // Keep the temp file alive let _keep_alive = config_path; // Keep the temp file alive
let mut reader = BufReader::new(stdout).lines(); let mut reader = BufReader::new(stdout).lines();
let mut current_track: f64 = 0.0; let mut current_track: f64 = 0.0;
@@ -1207,6 +1207,16 @@ pub fn run() {
crate::scheduler::spawn_scheduler(app.handle().clone()); crate::scheduler::spawn_scheduler(app.handle().clone());
#[cfg(target_os = "macos")]
{
use tauri::Manager;
use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
if let Some(window) = app.get_webview_window("main") {
apply_vibrancy(&window, NSVisualEffectMaterial::Sidebar, None, None)
.expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
}
}
let resource_dir = app.path().resource_dir().unwrap(); let resource_dir = app.path().resource_dir().unwrap();
let aria2c_path = resource_dir.join("binaries").join("aria2c"); let aria2c_path = resource_dir.join("binaries").join("aria2c");
@@ -1233,7 +1243,7 @@ pub fn run() {
let app_handle_clone = app.handle().clone(); let app_handle_clone = app.handle().clone();
let aria2_port_clone = aria2_port; let aria2_port_clone = aria2_port;
let aria2_secret_clone = aria2_secret.clone(); let aria2_secret_clone = aria2_secret.clone();
tokio::spawn(async move { tauri::async_runtime::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(500)).await; tokio::time::sleep(std::time::Duration::from_millis(500)).await;
let ws_url = format!("ws://127.0.0.1:{}/jsonrpc", aria2_port_clone); let ws_url = format!("ws://127.0.0.1:{}/jsonrpc", aria2_port_clone);
+1 -1
View File
@@ -20,7 +20,7 @@ struct SchedulerSettings {
} }
pub fn spawn_scheduler(app_handle: tauri::AppHandle) { pub fn spawn_scheduler(app_handle: tauri::AppHandle) {
tokio::spawn(async move { tauri::async_runtime::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(10)); let mut interval = tokio::time::interval(Duration::from_secs(10));
loop { loop {
interval.tick().await; interval.tick().await;
+4 -2
View File
@@ -18,12 +18,14 @@
"minWidth": 800, "minWidth": 800,
"minHeight": 600, "minHeight": 600,
"titleBarStyle": "Overlay", "titleBarStyle": "Overlay",
"hiddenTitle": true "hiddenTitle": true,
"transparent": true
} }
], ],
"security": { "security": {
"csp": null "csp": null
} },
"macOSPrivateApi": true
}, },
"bundle": { "bundle": {
"active": true, "active": true,
+14 -16
View File
@@ -13,12 +13,7 @@ import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link";
import SchedulerView from "./components/SchedulerView"; import SchedulerView from "./components/SchedulerView";
import SpeedLimiterView from "./components/SpeedLimiterView"; import SpeedLimiterView from "./components/SpeedLimiterView";
const localDateKey = (date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
const handleDeepLinks = (deepLinks: string[]) => { const handleDeepLinks = (deepLinks: string[]) => {
for (const rawDeepLink of deepLinks) { for (const rawDeepLink of deepLinks) {
@@ -239,21 +234,24 @@ function App() {
}, []); }, []);
return ( return (
<div className="app-shell flex h-screen w-screen text-text-primary overflow-hidden"> <div className="app-shell flex h-screen w-screen overflow-hidden text-text-primary">
{/* Left Side Panel - Curved Second Layer on Top */}
<div <div
className={`app-sidebar flex flex-col overflow-hidden relative z-20 shrink-0 transition-all duration-300 ease-in-out ${ className={`app-sidebar-shell relative z-20 shrink-0 transition-all duration-300 ease-in-out ${
isSidebarVisible ? 'w-[244px] opacity-100' : 'w-0 opacity-0' isSidebarVisible ? 'w-[244px] opacity-100' : 'w-0 opacity-0 pointer-events-none'
}`} }`}
> >
<div className="w-[244px] h-full flex flex-col shrink-0"> <div className="app-sidebar-panel h-full w-[244px]">
<Sidebar selectedFilter={filter} onSelectFilter={(f) => { setFilter(f); useSettingsStore.getState().setActiveView('downloads'); }} /> <Sidebar
selectedFilter={filter}
onSelectFilter={(f) => {
setFilter(f);
useSettingsStore.getState().setActiveView('downloads');
}}
/>
</div> </div>
</div> </div>
{/* Main Content - Base Layer */} <div className="app-workspace relative z-0 flex-1 flex flex-col h-full overflow-hidden">
<div className="app-workspace flex-1 flex flex-col h-full relative z-0">
<div className="flex-1 flex flex-col overflow-hidden relative"> <div className="flex-1 flex flex-col overflow-hidden relative">
{activeView === 'downloads' && <DownloadTable filter={filter} />} {activeView === 'downloads' && <DownloadTable filter={filter} />}
{activeView === 'settings' && <SettingsView />} {activeView === 'settings' && <SettingsView />}
@@ -264,7 +262,7 @@ function App() {
{/* Status Bar */} {/* Status Bar */}
<div className="app-statusbar h-8 px-5 flex items-center justify-between text-[10px] text-text-muted font-medium shrink-0 border-t border-border-color"> <div className="app-statusbar h-8 px-5 flex items-center justify-between text-[10px] text-text-muted font-medium shrink-0 border-t border-border-color">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500"></span> <span className="w-1.5 h-1.5 rounded-full bg-[hsl(var(--status-success))]"></span>
Ready Ready
</span> </span>
<div className="flex gap-3 tabular-nums"> <div className="flex gap-3 tabular-nums">
+155 -153
View File
@@ -5,7 +5,6 @@ import { SidebarFilter } from './Sidebar';
import { Play, Pause, Plus, Trash2, FileText, Image as ImageIcon, Music, Film, Box, Archive, FileQuestion, MoreVertical, PanelLeft } from 'lucide-react'; import { Play, Pause, Plus, Trash2, FileText, Image as ImageIcon, Music, Film, Box, Archive, FileQuestion, MoreVertical, PanelLeft } from 'lucide-react';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { homeDir } from '@tauri-apps/api/path'; import { homeDir } from '@tauri-apps/api/path';
import { WindowDragRegion } from './WindowDragRegion';
interface DownloadTableProps { interface DownloadTableProps {
filter: SidebarFilter; filter: SidebarFilter;
@@ -13,7 +12,7 @@ interface DownloadTableProps {
export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => { export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
const { downloads, toggleAddModal, updateDownload, removeDownload, clearFinished, redownload } = useDownloadStore(); const { downloads, toggleAddModal, updateDownload, removeDownload, clearFinished, redownload } = useDownloadStore();
const { isSidebarVisible, toggleSidebar, listRowDensity } = useSettingsStore(); const { isSidebarVisible, toggleSidebar } = useSettingsStore();
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null); const [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null);
@@ -87,11 +86,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
}; };
const contextItem = contextMenu ? downloads.find(d => d.id === contextMenu.id) : null; const contextItem = contextMenu ? downloads.find(d => d.id === contextMenu.id) : null;
const rowPadding = {
compact: 'py-2',
standard: 'py-2.5',
relaxed: 'py-3.5'
}[listRowDensity];
const getCategoryIcon = (category: string) => { const getCategoryIcon = (category: string) => {
switch(category) { switch(category) {
@@ -107,174 +102,181 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
} }
return ( return (
<div className="flex-1 flex flex-col bg-transparent h-full relative p-3 pb-0"> <div className="downloads-view flex-1 flex flex-col h-full min-w-0">
<div className="app-surface shrink-0 rounded-xl mb-3 z-10"> <div className={`main-titlebar ${!isSidebarVisible ? 'pl-[80px]' : ''}`} data-tauri-drag-region>
<WindowDragRegion className={!isSidebarVisible ? 'pl-20' : ''} /> {!isSidebarVisible && (
{/* Download Toolbar */}
<div className={`flex px-3 pb-2.5 pt-0.5 items-center ${!isSidebarVisible ? 'pl-20' : ''}`}>
<button <button
onClick={toggleSidebar} onClick={toggleSidebar}
className="app-icon-button mr-2" className="app-icon-button relative z-50 h-7 w-7 mr-2"
title="Toggle Sidebar" title="Show Sidebar"
> >
<PanelLeft size={17} strokeWidth={2} /> <PanelLeft size={16} strokeWidth={2} />
</button> </button>
<div className="mr-auto"> )}
<h2 className="text-[14px] font-semibold text-text-primary tracking-tight cursor-default">{getFilterTitle()}</h2> <div className="main-titlebar-title cursor-default" data-tauri-drag-region>Firelink</div>
<p className="mt-0.5 text-[10px] text-text-muted tabular-nums">
{filteredDownloads.length} {filteredDownloads.length === 1 ? 'item' : 'items'}
</p>
</div>
<div className="flex items-center gap-0.5"> <div className="main-control-group">
<button <button className="main-control-button primary" onClick={() => toggleAddModal(true)} title="Add Download">
onClick={() => toggleAddModal(true)} <Plus size={16} />
className="app-icon-button text-accent" </button>
title="Add Download"
> <button
<Plus size={17} strokeWidth={2.25} /> className="main-control-button"
</button> disabled={filteredDownloads.length === 0}
<div className="mx-1 h-4 w-px bg-border-color"></div> onClick={() => {
<button filteredDownloads.filter(d => d.status === 'paused').forEach(d => handleResume(d));
onClick={() => { }}
filteredDownloads.filter(d => d.status === 'paused').forEach(d => handleResume(d)); title="Resume All"
}} >
className="app-icon-button" <Play size={15} fill="currentColor" />
title="Resume All" </button>
>
<Play size={15} fill="currentColor" className="opacity-80" /> <button
</button> className="main-control-button"
<button disabled={filteredDownloads.length === 0}
onClick={() => { onClick={() => {
filteredDownloads.filter(d => d.status === 'downloading').forEach(d => handlePause(d.id)); filteredDownloads.filter(d => d.status === 'downloading').forEach(d => handlePause(d.id));
}} }}
className="app-icon-button" title="Pause All"
title="Pause All" >
> <Pause size={15} fill="currentColor" />
<Pause size={15} fill="currentColor" className="opacity-80" /> </button>
</button>
<div className="mx-1 h-4 w-px bg-border-color"></div> <button
<button className="main-control-button hover:!text-red-400"
onClick={clearFinished} disabled={filteredDownloads.length === 0}
className="app-icon-button hover:text-red-400" onClick={clearFinished}
title="Clear Finished" title="Clear Finished"
> >
<Trash2 size={15} strokeWidth={1.9} /> <Trash2 size={15} />
</button> </button>
</div>
</div> </div>
</div> </div>
{/* List */} <div className="downloads-content-header">
<div className="flex-1 overflow-auto bg-transparent pb-3 relative"> <div className="downloads-title">
<div className="w-full text-left"> {getFilterTitle()}
<div className="flex text-text-muted text-[9px] font-bold tracking-[0.12em] uppercase px-4 pb-2 pt-1 sticky top-0 z-0 bg-main-bg/85 backdrop-blur-md"> <span className="downloads-count">{filteredDownloads.length}</span>
<div className="flex-1 min-w-[200px]">FILE</div> </div>
<div className="w-32">SIZE</div> </div>
<div className="w-32">STATUS</div>
<div className="w-28">SPEED</div> <div className="downloads-table flex-1 flex flex-col">
<div className="w-28">ETA</div> <div className="download-table-header">
<div className="w-32 text-right pr-4">DATE ADDED</div> <div>File Name</div>
</div> <div>Size</div>
<div className="flex flex-col gap-2"> <div>Status</div>
{filteredDownloads.length === 0 ? ( <div>Speed</div>
<div className="app-card mx-1 mt-2 py-14 text-center"> <div>ETA</div>
<div className="flex flex-col items-center justify-center gap-3"> <div className="download-cell-right">Date Added</div>
<div className="flex h-12 w-12 items-center justify-center rounded-2xl border border-border-color bg-bg-input text-text-muted"> </div>
<Box size={23} strokeWidth={1.6} />
</div> <div className="download-table-body">
<div> {filteredDownloads.length === 0 ? (
<p className="text-[13px] font-semibold text-text-primary">No downloads here</p> <div className="h-full overflow-auto">
<p className="mt-1 text-[11px] text-text-muted">Add a link to start a new transfer.</p> <div className="download-row download-empty-row">
</div> <div className="download-file-cell">
<button onClick={() => toggleAddModal(true)} className="app-button app-button-primary mt-1 px-3 text-[11px]"> <FileQuestion size={15} />
<Plus size={14} /> Add Download <span className="download-file-name">
</button> No downloads yet · Use + to add a link
</span>
</div> </div>
<div></div>
<div>Idle</div>
<div></div>
<div></div>
<div className="download-cell-right"></div>
</div> </div>
) : (
filteredDownloads.map(d => ( {Array.from({ length: 12 }).map((_, index) => (
<div key={index} className="download-ghost-row" />
))}
</div>
) : (
<div className="h-full overflow-auto">
{filteredDownloads.map(d => (
<div <div
key={d.id} key={d.id}
className={`group mx-1 flex items-center rounded-lg border border-border-color bg-bg-modal/30 px-4 ${rowPadding} cursor-default transition-colors duration-150 hover:border-border-modal hover:bg-item-hover/50`} className="download-row group cursor-default relative"
onContextMenu={(e) => { onContextMenu={(e) => {
e.preventDefault(); e.preventDefault();
setContextMenu({ setContextMenu({ x: e.clientX, y: e.clientY, id: d.id });
x: e.clientX,
y: e.clientY,
id: d.id
});
}} }}
> >
<div className="flex-1 min-w-[200px] text-[13px] text-text-primary pr-4"> <div className="download-file-cell">
<div className="flex items-center gap-3"> <span className="shrink-0 text-text-muted">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[10px] border border-border-color bg-bg-input text-text-muted"> {getCategoryIcon(d.category)}
{getCategoryIcon(d.category)} </span>
</div> <span className="download-file-name">
<span className="font-medium truncate max-w-[280px]">{d.fileName}</span> {d.fileName}
</div>
</div>
<div className="w-32 pr-4">
{d.status === 'downloading' || d.status === 'paused' ? (
<div className="w-full">
<div className="w-full bg-border-color/30 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-accent'}`} 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>
)}
</div>
<div className="w-32">
<span className={`inline-flex items-center px-2 py-0.5 rounded-md text-[10px] font-bold tracking-widest uppercase shadow-sm ${
d.status === 'completed' ? 'bg-green-500/10 text-green-500 border border-green-500/20' :
d.status === 'downloading' ? 'bg-accent/10 text-accent border border-accent/20' :
d.status === 'failed' ? 'bg-red-500/10 text-red-500 border border-red-500/20' :
d.status === 'paused' ? 'bg-orange-500/10 text-orange-500 border border-orange-500/20' :
'bg-item-hover text-text-muted border border-border-color/30'
}`}>
{d.status}
</span> </span>
</div> </div>
<div className="w-28 text-[12px] text-text-secondary font-medium">{d.speed}</div>
<div className="w-28 text-[12px] text-text-secondary font-medium">{d.eta}</div> <div>
<div className="w-32 relative text-right pr-4"> <span className="tabular-nums">
<div className="flex items-center justify-end"> {d.status === 'downloading' || d.status === 'paused'
<span className="text-[12px] text-text-secondary font-medium group-hover:opacity-0 transition-opacity duration-200"> ? `${((d.fraction || 0) * parseInt((d.size || '').replace(/[^0-9.]/g, '') || '0')).toFixed(1)} GB / ${d.size || '-'}`
{d.dateAdded ? new Date(d.dateAdded).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) : '-'} : d.size || '-'}
</span> </span>
<div className="flex justify-end gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity duration-200 absolute right-4 top-1/2 -translate-y-1/2"> </div>
{d.status === 'downloading' && (
<button onClick={() => handlePause(d.id)} className="app-icon-button h-7 w-7 border border-border-color bg-bg-modal hover:text-orange-400" title="Pause"> <div>
<Pause size={14} fill="currentColor" /> <div className="flex flex-col justify-center gap-1.5">
</button> <div className="download-progress-status">
)} <span className={`truncate text-[12px] ${d.status === 'completed' ? 'download-status-completed' : d.status === 'paused' ? 'download-status-paused' : d.status === 'failed' ? 'download-status-failed' : 'download-status-downloading'}`}>
{d.status === 'paused' && ( {d.status === 'downloading' || d.status === 'paused' ? `${((d.fraction || 0) * 100).toFixed(0)}%` : d.status.charAt(0).toUpperCase() + d.status.slice(1)}
<button onClick={() => handleResume(d)} className="app-icon-button h-7 w-7 border border-border-color bg-bg-modal hover:text-green-400" title="Resume"> </span>
<Play size={14} fill="currentColor" />
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
setContextMenu({ x: e.clientX, y: e.clientY, id: d.id });
}}
className="app-icon-button h-7 w-7 border border-border-color bg-bg-modal hover:text-accent"
title="More Actions"
>
<MoreVertical size={14} />
</button>
</div> </div>
{(d.status === 'downloading' || d.status === 'paused') && (
<div className="download-progress-track">
<div className={`download-progress-fill transition-all duration-300 ease-out ${d.status === 'paused' ? 'paused' : ''}`} style={{ width: `${(d.fraction || 0) * 100}%` }}></div>
</div>
)}
</div>
</div>
<div>
<span className="tabular-nums">{d.status === 'downloading' ? d.speed : '-'}</span>
</div>
<div>
<span className="tabular-nums">{d.status === 'downloading' ? d.eta : '-'}</span>
</div>
<div className="download-cell-right">
<span className="truncate group-hover:hidden tabular-nums ml-auto">
{d.dateAdded ? new Date(d.dateAdded).toLocaleDateString() : '-'}
</span>
<div className="hidden group-hover:flex items-center justify-end gap-0.5 w-full ml-auto">
{d.status === 'downloading' && (
<button onClick={() => handlePause(d.id)} className="app-icon-button h-7 w-7" title="Pause">
<Pause size={14} fill="currentColor" />
</button>
)}
{d.status === 'paused' && (
<button onClick={() => handleResume(d)} className="app-icon-button h-7 w-7" title="Resume">
<Play size={14} fill="currentColor" />
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
setContextMenu({ x: e.clientX, y: e.clientY, id: d.id });
}}
className="app-icon-button h-7 w-7"
title="Options"
>
<MoreVertical size={14} />
</button>
</div> </div>
</div> </div>
</div> </div>
)) ))}
)} {Array.from({ length: Math.max(0, 10 - filteredDownloads.length) }).map((_, index) => (
</div> <div key={`ghost-${index}`} className="download-ghost-row" />
))}
</div>
)}
</div> </div>
</div> </div>
+144 -212
View File
@@ -236,89 +236,69 @@ export default function SettingsView() {
{/* Downloads Pane */} {/* Downloads Pane */}
{activeTab === 'downloads' && ( {activeTab === 'downloads' && (
<div className="settings-pane space-y-6 max-w-[760px]"> <div className="settings-pane max-w-[760px]">
<h3 className="text-base font-bold text-text-primary border-b border-border-color/30 pb-2">Download Options</h3> <div className="mac-settings-group">
<div className="mac-settings-row">
<div className="grid grid-cols-[180px_1fr] items-center gap-4 text-[13px]"> <span className="text-[13px] text-text-primary">Parallel downloads</span>
<label className="text-text-secondary font-medium">Parallel downloads:</label> <div className="flex items-center gap-3">
<div className="flex items-center gap-4"> <span className="text-text-muted text-[13px] w-6 text-right">{settings.maxConcurrentDownloads}</span>
<input <input
type="range" min="1" max="12" type="range" min="1" max="12"
value={settings.maxConcurrentDownloads} value={settings.maxConcurrentDownloads}
onChange={(e) => settings.setMaxConcurrentDownloads(Number(e.target.value))} onChange={(e) => settings.setMaxConcurrentDownloads(Number(e.target.value))}
className="flex-1 accent-accent" className="accent-accent w-24"
/> />
<span className="w-8 text-center font-mono font-bold bg-item-hover px-2 py-1 rounded border border-border-modal text-text-secondary"> </div>
{settings.maxConcurrentDownloads}
</span>
</div> </div>
</div> <div className="mac-settings-row">
<span className="text-[13px] text-text-primary">Default connections</span>
<div className="grid grid-cols-[180px_1fr] items-center gap-4 text-[13px]">
<label className="text-text-secondary font-medium">Default connections:</label>
<div className="flex items-center gap-4">
<input <input
type="number" min="1" max="16" type="number" min="1" max="16"
value={settings.perServerConnections} value={settings.perServerConnections}
onChange={(e) => settings.setPerServerConnections(Number(e.target.value))} onChange={(e) => settings.setPerServerConnections(Number(e.target.value))}
className="bg-bg-input border border-border-modal rounded-md px-3 py-1.5 w-24 text-text-primary focus:outline-none focus:border-accent" className="app-control w-16 text-center"
/> />
<span className="text-text-muted text-xs">For new downloads (1 to 16)</span>
</div> </div>
</div> <div className="mac-settings-row">
<span className="text-[13px] text-text-primary">Global speed limit</span>
<div className="grid grid-cols-[180px_1fr] items-center gap-4 text-[13px]">
<label className="text-text-secondary font-medium">Global speed limit:</label>
<div className="flex items-center gap-4">
<input <input
type="text" type="text"
value={settings.globalSpeedLimit} value={settings.globalSpeedLimit}
onChange={(e) => settings.setGlobalSpeedLimit(e.target.value)} onChange={(e) => settings.setGlobalSpeedLimit(e.target.value)}
placeholder="Unlimited" placeholder="0"
className="bg-bg-input border border-border-modal rounded-md px-3 py-1.5 w-32 font-mono text-text-primary focus:outline-none focus:border-accent" className="app-control w-24 text-center font-mono"
/> />
<span className="text-text-muted text-xs">e.g. 500K, 1M, or 0 for unlimited</span>
</div> </div>
</div> <div className="mac-settings-row">
<span className="text-[13px] text-text-primary">Automatic retries</span>
<div className="grid grid-cols-[180px_1fr] items-center gap-4 text-[13px]">
<label className="text-text-secondary font-medium">Automatic retries:</label>
<div className="flex items-center gap-4">
<input <input
type="number" min="0" max="10" type="number" min="0" max="10"
value={settings.maxAutomaticRetries} value={settings.maxAutomaticRetries}
onChange={(e) => settings.setMaxAutomaticRetries(Number(e.target.value))} onChange={(e) => settings.setMaxAutomaticRetries(Number(e.target.value))}
className="bg-bg-input border border-border-modal rounded-md px-3 py-1.5 w-24 text-text-primary focus:outline-none focus:border-accent" className="app-control w-16 text-center"
/> />
<span className="text-text-muted text-xs">If a connection fails (0 to 10)</span>
</div> </div>
</div> </div>
<div className="border-t border-border-color/30 pt-4 space-y-3"> <div className="mac-settings-group">
<label className="flex items-start gap-3 cursor-default select-none text-[13px] text-text-secondary"> <label className="mac-settings-row cursor-default">
<span className="text-[13px] text-text-primary">Show completion notifications</span>
<input <input
type="checkbox" type="checkbox"
checked={settings.showNotifications} checked={settings.showNotifications}
onChange={(e) => settings.setShowNotifications(e.target.checked)} onChange={(e) => settings.setShowNotifications(e.target.checked)}
className="mt-0.5 rounded accent-accent" className="mac-switch"
/> />
<div>
<p className="font-semibold text-text-primary">Show notification when download completes</p>
<p className="text-text-muted text-xs mt-0.5">Alerts you in the System Notification Center</p>
</div>
</label> </label>
<label className="mac-settings-row cursor-default" style={{ opacity: settings.showNotifications ? 1 : 0.5 }}>
<label className="flex items-start gap-3 cursor-default select-none text-[13px] text-text-secondary pl-6"> <span className="text-[13px] text-text-primary">Play completion sound</span>
<input <input
type="checkbox" type="checkbox"
checked={settings.playCompletionSound && settings.showNotifications} checked={settings.playCompletionSound}
disabled={!settings.showNotifications} disabled={!settings.showNotifications}
onChange={(e) => settings.setPlayCompletionSound(e.target.checked)} onChange={(e) => settings.setPlayCompletionSound(e.target.checked)}
className="mt-0.5 rounded accent-accent disabled:opacity-40" className="mac-switch"
/> />
<div>
<p className={`font-semibold ${settings.showNotifications ? 'text-text-primary' : 'text-text-muted'}`}>Play sound when download completes</p>
</div>
</label> </label>
</div> </div>
</div> </div>
@@ -326,89 +306,69 @@ export default function SettingsView() {
{/* Look & Feel Pane */} {/* Look & Feel Pane */}
{activeTab === 'lookandfeel' && ( {activeTab === 'lookandfeel' && (
<div className="settings-pane space-y-6 max-w-[760px]"> <div className="settings-pane max-w-[760px]">
<h3 className="text-base font-semibold text-text-primary border-b border-border-color pb-2">App Theme</h3> <div className="mac-settings-group">
<div className="mac-settings-row">
<div className="grid grid-cols-[180px_1fr] items-start gap-4 text-[13px]"> <span className="text-[13px] text-text-primary">App Theme</span>
<label className="text-text-secondary font-medium pt-1">Theme:</label> <select
<div className="space-y-2"> value={settings.theme}
{[ onChange={(e) => settings.setTheme(e.target.value as any)}
{ value: 'system', label: 'System Default' }, className="app-control w-40"
{ value: 'light', label: 'Light' }, >
{ value: 'dark', label: 'Dark' }, <option value="system">System Default</option>
{ value: 'dracula', label: 'Dracula' }, <option value="light">Light</option>
{ value: 'nord', label: 'Nord' }, <option value="dark">Dark</option>
].map(({ value, label }) => ( <option value="dracula">Dracula</option>
<label key={value} className="flex items-center gap-2 cursor-default select-none text-text-primary"> <option value="nord">Nord</option>
<input </select>
type="radio"
name="themeRadio"
value={value}
checked={settings.theme === value}
onChange={() => settings.setTheme(value as typeof settings.theme)}
className="accent-accent"
/>
{label}
</label>
))}
<p className="text-text-muted text-xs mt-2">Select a color palette for the app's user interface.</p>
</div> </div>
</div> </div>
<h3 className="text-base font-semibold text-text-primary border-b border-border-color pb-2 pt-2">Display</h3> <div className="mac-settings-group">
<div className="mac-settings-row">
<div className="grid grid-cols-[180px_1fr] items-center gap-4 text-[13px]"> <span className="text-[13px] text-text-primary">Font Size</span>
<label className="text-text-secondary font-medium">Font Size:</label> <select
<select value={settings.appFontSize}
value={settings.appFontSize} onChange={(e) => settings.setAppFontSize(e.target.value as any)}
onChange={(e) => settings.setAppFontSize(e.target.value as any)} className="app-control w-40"
className="bg-bg-input border border-border-modal rounded-md px-3 py-1.5 text-[13px] text-text-primary focus:outline-none focus:border-accent max-w-[200px]" >
> <option value="small">Small</option>
<option value="small">Small</option> <option value="standard">Standard</option>
<option value="standard">Standard</option> <option value="large">Large</option>
<option value="large">Large</option> </select>
</select> </div>
<div className="mac-settings-row">
<span className="text-[13px] text-text-primary">List Row Density</span>
<select
value={settings.listRowDensity}
onChange={(e) => settings.setListRowDensity(e.target.value as any)}
className="app-control w-40"
>
<option value="compact">Compact</option>
<option value="standard">Standard</option>
<option value="relaxed">Relaxed</option>
</select>
</div>
</div> </div>
<div className="grid grid-cols-[180px_1fr] items-center gap-4 text-[13px]"> <div className="mac-settings-group">
<label className="text-text-secondary font-medium">List Row Density:</label> <label className="mac-settings-row cursor-default">
<select <span className="text-[13px] text-text-primary">Show badge on Dock icon</span>
value={settings.listRowDensity}
onChange={(e) => settings.setListRowDensity(e.target.value as any)}
className="bg-bg-input border border-border-modal rounded-md px-3 py-1.5 text-[13px] text-text-primary focus:outline-none focus:border-accent max-w-[200px]"
>
<option value="compact">Compact</option>
<option value="standard">Standard</option>
<option value="relaxed">Relaxed</option>
</select>
</div>
<h3 className="text-base font-semibold text-text-primary border-b border-border-color pb-2 pt-2">macOS Integration</h3>
<div className="space-y-4">
<label className="flex items-start gap-3 cursor-default select-none text-[13px] text-text-secondary">
<input <input
type="checkbox" type="checkbox"
checked={settings.showDockBadge} checked={settings.showDockBadge}
onChange={(e) => settings.setShowDockBadge(e.target.checked)} onChange={(e) => settings.setShowDockBadge(e.target.checked)}
className="mt-0.5 rounded accent-accent" className="mac-switch"
/> />
<div>
<p className="font-semibold text-text-primary">Show badge on Dock/Taskbar icon</p>
<p className="text-text-muted text-xs mt-0.5">Displays the number of active downloads on the icon badge.</p>
</div>
</label> </label>
<label className="flex items-start gap-3 cursor-default select-none text-[13px] text-text-secondary"> <label className="mac-settings-row cursor-default">
<span className="text-[13px] text-text-primary">Show menu bar icon</span>
<input <input
type="checkbox" type="checkbox"
checked={settings.showMenuBarIcon} checked={settings.showMenuBarIcon}
onChange={(e) => settings.setShowMenuBarIcon(e.target.checked)} onChange={(e) => settings.setShowMenuBarIcon(e.target.checked)}
className="mt-0.5 rounded accent-accent" className="mac-switch"
/> />
<div>
<p className="font-semibold text-text-primary">Show menu bar icon</p>
<p className="text-text-muted text-xs mt-0.5">Provides quick access to downloads and queues from the system menu bar.</p>
</div>
</label> </label>
</div> </div>
</div> </div>
@@ -416,77 +376,55 @@ export default function SettingsView() {
{/* Network Pane */} {/* Network Pane */}
{activeTab === 'network' && ( {activeTab === 'network' && (
<div className="settings-pane space-y-6 max-w-[760px]"> <div className="settings-pane max-w-[760px]">
<h3 className="text-base font-bold text-text-primary border-b border-border-color/30 pb-2">Proxy & User Agent</h3> <div className="mac-settings-group">
<div className="mac-settings-row">
<div className="grid grid-cols-[180px_1fr] items-start gap-4 text-[13px]"> <span className="text-[13px] text-text-primary">Proxy Mode</span>
<label className="text-text-secondary font-medium pt-1">Proxy Mode:</label> <select
<div className="space-y-2"> value={settings.proxyMode}
<label className="flex items-center gap-2 cursor-default select-none text-text-secondary"> onChange={(e) => settings.setProxyMode(e.target.value as any)}
<input className="app-control w-40"
type="radio" name="proxyMode" value="none" >
checked={settings.proxyMode === 'none'} <option value="none">No proxy</option>
onChange={() => settings.setProxyMode('none')} <option value="system">Use system proxy</option>
className="accent-accent" <option value="custom">Set custom proxy</option>
/> </select>
No proxy
</label>
<label className="flex items-center gap-2 cursor-default select-none text-text-secondary">
<input
type="radio" name="proxyMode" value="system"
checked={settings.proxyMode === 'system'}
onChange={() => settings.setProxyMode('system')}
className="accent-accent"
/>
Use system proxy
</label>
<label className="flex items-center gap-2 cursor-default select-none text-text-secondary">
<input
type="radio" name="proxyMode" value="custom"
checked={settings.proxyMode === 'custom'}
onChange={() => settings.setProxyMode('custom')}
className="accent-accent"
/>
Set proxy
</label>
</div> </div>
{settings.proxyMode === 'custom' && (
<>
<div className="mac-settings-row">
<span className="text-[13px] text-text-primary pl-4">Proxy Host</span>
<input
type="text"
value={settings.proxyHost}
onChange={(e) => settings.setProxyHost(e.target.value)}
placeholder="127.0.0.1"
className="app-control w-40 font-mono"
/>
</div>
<div className="mac-settings-row">
<span className="text-[13px] text-text-primary pl-4">Proxy Port</span>
<input
type="number"
value={settings.proxyPort}
onChange={(e) => settings.setProxyPort(Number(e.target.value))}
className="app-control w-24 text-center"
/>
</div>
</>
)}
</div> </div>
{settings.proxyMode === 'custom' && ( <div className="mac-settings-group">
<div className="bg-item-hover/30 border border-border-modal rounded-lg p-4 pl-6 space-y-4 max-w-[420px] ml-[180px]"> <div className="mac-settings-row">
<div className="grid grid-cols-[80px_1fr] items-center gap-2 text-[13px]"> <span className="text-[13px] text-text-primary">Custom User Agent</span>
<label className="text-text-secondary">Host:</label>
<input
type="text"
value={settings.proxyHost}
onChange={(e) => settings.setProxyHost(e.target.value)}
placeholder="127.0.0.1"
className="bg-bg-input border border-border-modal rounded-md px-3 py-1 text-text-primary font-mono text-xs focus:outline-none"
/>
</div>
<div className="grid grid-cols-[80px_1fr] items-center gap-2 text-[13px]">
<label className="text-text-secondary">Port:</label>
<input
type="number"
value={settings.proxyPort}
onChange={(e) => settings.setProxyPort(Number(e.target.value))}
className="bg-bg-input border border-border-modal rounded-md px-3 py-1 text-text-primary font-mono text-xs w-[100px] focus:outline-none"
/>
</div>
</div>
)}
<div className="grid grid-cols-[180px_1fr] items-center gap-4 text-[13px] border-t border-border-color/30 pt-4">
<label className="text-text-secondary font-medium">User Agent:</label>
<div className="space-y-1">
<input <input
type="text" type="text"
value={settings.customUserAgent} value={settings.customUserAgent}
onChange={(e) => settings.setCustomUserAgent(e.target.value)} onChange={(e) => settings.setCustomUserAgent(e.target.value)}
placeholder="e.g. Mozilla/5.0..." placeholder="e.g. Mozilla/5.0..."
className="bg-bg-input border border-border-modal rounded-md px-3 py-1.5 w-full font-mono text-[11px] text-text-primary focus:outline-none focus:border-accent" className="app-control flex-1 ml-4 font-mono text-[11px]"
/> />
<p className="text-text-muted text-xs">Spoofs browser User-Agent to bypass download restrictions. Leave blank for default.</p>
</div> </div>
</div> </div>
</div> </div>
@@ -494,69 +432,63 @@ export default function SettingsView() {
{/* Locations Pane */} {/* Locations Pane */}
{activeTab === 'locations' && ( {activeTab === 'locations' && (
<div className="settings-pane space-y-6 max-w-[760px]"> <div className="settings-pane max-w-[760px]">
<h3 className="text-base font-bold text-text-primary border-b border-border-color/30 pb-2">Download Directories</h3> <div className="mac-settings-group">
<label className="mac-settings-row cursor-default">
<span className="text-[13px] text-text-primary">Ask where to save each file</span>
<input
type="checkbox"
checked={settings.askWhereToSaveEachFile}
onChange={(e) => settings.setAskWhereToSaveEachFile(e.target.checked)}
className="mac-switch"
/>
</label>
</div>
<label className="flex items-start gap-3 cursor-default select-none text-[13px] text-text-secondary"> <div className="mac-settings-group">
<input <div className="mac-settings-row bg-item-hover/20">
type="checkbox" <span className="text-[13px] font-semibold text-text-primary">All Categories Base</span>
checked={settings.askWhereToSaveEachFile}
onChange={(e) => settings.setAskWhereToSaveEachFile(e.target.checked)}
className="mt-0.5 rounded accent-accent"
/>
<div>
<p className="font-semibold text-text-primary">Ask where to save each file before downloading</p>
<p className="text-text-muted text-xs mt-0.5">When enabled, you choose the download location each time you add links.</p>
</div>
</label>
<div className="space-y-4 border-t border-border-color/30 pt-4">
<h4 className="text-[13px] font-bold text-text-primary">Default Categories Paths</h4>
{/* Bulk Directory Selector */}
<div className="grid grid-cols-[150px_1fr] items-center gap-4 text-[13px] bg-item-hover/35 p-3 rounded-lg border border-border-modal/40">
<label className="font-semibold text-text-primary">All Categories Base:</label>
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
type="text" readOnly placeholder="Choose base folder to sub-categorize..." type="text" readOnly placeholder="Choose base folder..."
className="flex-1 bg-bg-input border border-border-modal rounded-md px-3 py-1 text-xs text-text-muted" className="app-control w-48 text-text-muted text-[11px]"
/> />
<button <button
onClick={handleBrowseBulk} onClick={handleBrowseBulk}
className="bg-accent hover:bg-accent text-white px-3 py-1 rounded-md text-xs font-semibold shadow transition-colors" className="app-icon-button bg-accent/10 hover:bg-accent/20 text-accent font-semibold px-3"
> >
Choose Base Browse
</button> </button>
</div> </div>
</div> </div>
{['Musics', 'Movies', 'Compressed', 'Documents', 'Pictures', 'Applications', 'Other'].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]"> <div key={category} className="mac-settings-row">
<label className="text-text-secondary capitalize">{category} folder:</label> <span className="text-[13px] text-text-primary pl-4">{category}</span>
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
type="text" type="text"
value={(settings.downloadDirectories || {})[category] || ''} value={(settings.downloadDirectories || {})[category] || ''}
onChange={(e) => settings.setCategoryDirectory(category, e.target.value)} 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" className="app-control w-48 text-[11px]"
/> />
<button <button
onClick={() => handleBrowseCategory(category)} onClick={() => handleBrowseCategory(category)}
className="bg-item-hover hover:bg-item-hover/80 text-text-primary border border-border-modal px-2.5 py-1 rounded-md text-xs" className="app-icon-button hover:bg-item-hover text-text-secondary px-3"
> >
Choose Browse
</button> </button>
</div> </div>
</div> </div>
))} ))}
<div className="flex justify-end gap-2 pt-2 border-t border-border-color/30"> <div className="mac-settings-row justify-end border-t-0">
<button <button
onClick={() => { onClick={() => {
settings.resetCategoryDirectories(); settings.resetCategoryDirectories();
showToast("Reset directories to default"); showToast("Reset directories to default");
}} }}
className="bg-item-hover hover:bg-item-hover/80 text-text-primary border border-border-modal px-4 py-1.5 rounded-md text-xs" className="app-control hover:bg-item-hover text-text-secondary px-4 py-1"
> >
Reset Defaults Reset Defaults
</button> </button>
+37 -44
View File
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
import { import {
Inbox, Zap, CheckCircle2, CircleDashed, Inbox, Zap, CheckCircle2, CircleDashed,
Film, Music, FileText, Box, Image as ImageIcon, Archive, FileQuestion, Film, Music, FileText, Box, Image as ImageIcon, Archive, FileQuestion,
List, CalendarClock, Gauge, Settings, Plus, Play, Pause, Edit2, Trash2 List, CalendarClock, Gauge, Settings, Plus, Play, Pause, Edit2, Trash2, PanelLeft
} from 'lucide-react'; } from 'lucide-react';
import { useDownloadStore, DownloadCategory, Queue } from '../store/useDownloadStore'; import { useDownloadStore, DownloadCategory, Queue } from '../store/useDownloadStore';
import { ActiveView, useSettingsStore } from '../store/useSettingsStore'; import { ActiveView, useSettingsStore } from '../store/useSettingsStore';
@@ -18,7 +18,7 @@ interface SidebarProps {
export const Sidebar: React.FC<SidebarProps> = (props) => { export const Sidebar: React.FC<SidebarProps> = (props) => {
const { selectedFilter, onSelectFilter } = props; const { selectedFilter, onSelectFilter } = props;
const { downloads, queues, addQueue, renameQueue, removeQueue, startQueue, pauseQueue } = useDownloadStore(); const { downloads, queues, addQueue, renameQueue, removeQueue, startQueue, pauseQueue } = useDownloadStore();
const { activeView, setActiveView } = useSettingsStore(); const { activeView, setActiveView, toggleSidebar } = useSettingsStore();
const [isAddingQueue, setIsAddingQueue] = useState(false); const [isAddingQueue, setIsAddingQueue] = useState(false);
const [newQueueName, setNewQueueName] = useState(''); const [newQueueName, setNewQueueName] = useState('');
@@ -64,19 +64,13 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
<button <button
type="button" type="button"
data-active={isSelected} data-active={isSelected}
className={`sidebar-nav-item group flex h-8 w-full items-center px-3.5 rounded-lg text-[12px] text-left cursor-default transition-colors duration-150 mb-0.5 ${ className="sidebar-nav-item group flex w-full items-center text-[13px] text-left cursor-default font-medium"
isSelected
? 'bg-item-selected text-text-primary font-semibold'
: 'text-text-secondary hover:bg-item-hover hover:text-text-primary'
}`}
onClick={() => onSelectFilter(filter)} onClick={() => onSelectFilter(filter)}
> >
<Icon className={`w-4 h-4 mr-2.5 transition-colors ${isSelected ? 'text-accent' : 'text-text-muted group-hover:text-text-secondary'}`} strokeWidth={isSelected ? 2.25 : 1.8} /> <Icon className="w-[18px] h-[18px] mr-3 shrink-0" strokeWidth={isSelected ? 2.5 : 2} />
<span className="truncate">{label}</span> <span className="truncate">{label}</span>
{getCount(filter) > 0 && ( {getCount(filter) > 0 && (
<span className={`ml-auto min-w-5 px-1.5 py-0.5 rounded-full text-center text-[10px] leading-none font-semibold transition-colors ${ <span className="sidebar-count ml-auto min-w-5 px-1.5 py-0.5 rounded-full text-center text-[10px] leading-none font-bold">
isSelected ? 'bg-accent/15 text-accent' : 'bg-item-hover text-text-muted'
}`}>
{getCount(filter)} {getCount(filter)}
</span> </span>
)} )}
@@ -134,18 +128,12 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
data-active={isSelected} data-active={isSelected}
onContextMenu={e => handleQueueContextMenu(e, queue.id)} onContextMenu={e => handleQueueContextMenu(e, queue.id)}
onClick={() => onSelectFilter(filterId)} onClick={() => onSelectFilter(filterId)}
className={`sidebar-nav-item group flex h-8 w-full items-center px-3.5 rounded-lg text-[12px] text-left cursor-default transition-colors duration-150 mb-0.5 ${ className="sidebar-nav-item group flex w-full items-center text-[13px] text-left cursor-default font-medium"
isSelected
? 'bg-item-selected text-text-primary font-semibold'
: 'text-text-secondary hover:bg-item-hover hover:text-text-primary'
}`}
> >
<List className={`w-4 h-4 mr-2.5 shrink-0 transition-colors ${isSelected ? 'text-accent' : 'text-text-muted group-hover:text-text-secondary'}`} strokeWidth={isSelected ? 2.25 : 1.8} /> <List className="w-[18px] h-[18px] mr-3 shrink-0" strokeWidth={isSelected ? 2.5 : 2} />
<span className="truncate">{queue.name}</span> <span className="truncate">{queue.name}</span>
{getCount(filterId) > 0 && ( {getCount(filterId) > 0 && (
<span className={`ml-auto min-w-5 px-1.5 py-0.5 rounded-full text-center text-[10px] leading-none font-semibold shrink-0 transition-colors ${ <span className="sidebar-count ml-auto min-w-5 px-1.5 py-0.5 rounded-full text-center text-[10px] leading-none font-bold shrink-0">
isSelected ? 'bg-accent/15 text-accent' : 'bg-item-hover text-text-muted'
}`}>
{getCount(filterId)} {getCount(filterId)}
</span> </span>
)} )}
@@ -160,30 +148,39 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
type="button" type="button"
data-active={isSelected} data-active={isSelected}
onClick={() => setActiveView(view)} onClick={() => setActiveView(view)}
className={`sidebar-nav-item group flex h-8 w-full items-center px-3.5 rounded-lg text-[12px] text-left cursor-default transition-colors duration-150 mb-0.5 ${ className="sidebar-nav-item group flex w-full items-center text-[13px] text-left cursor-default font-medium"
isSelected ? 'bg-item-selected text-text-primary font-semibold' : 'text-text-secondary hover:bg-item-hover hover:text-text-primary'
}`}
> >
<Icon className={`w-4 h-4 mr-2.5 transition-colors ${isSelected ? 'text-accent' : 'text-text-muted group-hover:text-text-secondary'}`} strokeWidth={isSelected ? 2.25 : 1.8} /> <Icon className="w-[18px] h-[18px] mr-3 shrink-0" strokeWidth={isSelected ? 2.5 : 2} />
<span>{label}</span> <span>{label}</span>
</button> </button>
); );
}; };
return ( return (
<aside className="w-full h-full flex flex-col relative shrink-0"> <aside className="sidebar-inner">
<WindowDragRegion /> <div className="sidebar-top-region">
<div className="overflow-y-auto flex-1 px-3 pb-3"> <WindowDragRegion />
<section className="mb-4">
<div className="text-[9px] font-bold tracking-[0.14em] text-text-muted uppercase px-3.5 mb-1.5">Library</div> <button
type="button"
onClick={toggleSidebar}
className="sidebar-toggle-button"
title="Hide Sidebar"
>
<PanelLeft size={14} strokeWidth={1.9} />
</button>
</div>
<div className="sidebar-scroll">
<section className="sidebar-section">
<div className="sidebar-section-label">Library</div>
<NavItem icon={Inbox} label="All" filter="all" /> <NavItem icon={Inbox} label="All" filter="all" />
<NavItem icon={Zap} label="Active" filter="active" /> <NavItem icon={Zap} label="Active" filter="active" />
<NavItem icon={CheckCircle2} label="Completed" filter="completed" /> <NavItem icon={CheckCircle2} label="Completed" filter="completed" />
<NavItem icon={CircleDashed} label="Unfinished" filter="unfinished" /> <NavItem icon={CircleDashed} label="Unfinished" filter="unfinished" />
</section> </section>
<section className="mb-4"> <section className="sidebar-section">
<div className="text-[9px] font-bold tracking-[0.14em] text-text-muted uppercase px-3.5 mb-1.5">Folders</div> <div className="sidebar-section-label">Folders</div>
<NavItem icon={Music} label="Musics" filter="Musics" /> <NavItem icon={Music} label="Musics" filter="Musics" />
<NavItem icon={Film} label="Movies" filter="Movies" /> <NavItem icon={Film} label="Movies" filter="Movies" />
<NavItem icon={Archive} label="Compressed" filter="Compressed" /> <NavItem icon={Archive} label="Compressed" filter="Compressed" />
@@ -193,13 +190,13 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
<NavItem icon={FileQuestion} label="Other" filter="Other" /> <NavItem icon={FileQuestion} label="Other" filter="Other" />
</section> </section>
<section className="mb-4"> <section className="sidebar-section">
<div className="text-[9px] font-bold tracking-[0.14em] text-text-muted uppercase px-3.5 mb-1.5">Queues</div> <div className="sidebar-section-label">Queues</div>
{queues.map(queue => ( {queues.map(queue => (
<QueueItem key={queue.id} queue={queue} /> <QueueItem key={queue.id} queue={queue} />
))} ))}
{isAddingQueue ? ( {isAddingQueue ? (
<div className="flex items-center px-2.5 py-1 rounded-lg bg-item-hover"> <div className="flex items-center px-3.5 py-1.5 rounded-lg bg-item-hover mb-1">
<Plus className="w-4 h-4 mr-2 text-text-secondary shrink-0" strokeWidth={2} /> <Plus className="w-4 h-4 mr-2 text-text-secondary shrink-0" strokeWidth={2} />
<input <input
ref={addInputRef} ref={addInputRef}
@@ -219,7 +216,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
<button <button
type="button" type="button"
onClick={() => { setIsAddingQueue(true); setNewQueueName(''); }} onClick={() => { setIsAddingQueue(true); setNewQueueName(''); }}
className="flex w-full items-center px-3.5 py-1.5 rounded-lg text-[12px] text-text-muted hover:bg-item-hover hover:text-text-secondary cursor-default transition-colors" className="flex w-full items-center px-3.5 py-1.5 rounded-lg text-[13px] text-text-muted hover:bg-item-hover hover:text-text-secondary cursor-default transition-colors mb-1"
> >
<Plus className="w-4 h-4 mr-2 shrink-0" strokeWidth={2} /> <Plus className="w-4 h-4 mr-2 shrink-0" strokeWidth={2} />
<span className="truncate">Add new queue</span> <span className="truncate">Add new queue</span>
@@ -227,25 +224,21 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
)} )}
</section> </section>
<section className="mt-auto pt-4 border-t border-border-color/30"> <section className="sidebar-section">
<div className="text-[9px] font-bold tracking-[0.14em] text-text-muted uppercase px-3.5 mb-1.5">Tools</div> <div className="sidebar-section-label">Tools</div>
<ToolItem icon={CalendarClock} label="Scheduler" view="scheduler" /> <ToolItem icon={CalendarClock} label="Scheduler" view="scheduler" />
<ToolItem icon={Gauge} label="Speed Limiter" view="speedLimiter" /> <ToolItem icon={Gauge} label="Speed Limiter" view="speedLimiter" />
</section> </section>
</div> </div>
<div className="shrink-0 border-t border-border-color bg-sidebar-bg px-3 py-2"> <div className="sidebar-footer">
<button <button
type="button" type="button"
data-active={activeView === 'settings'} data-active={activeView === 'settings'}
onClick={() => setActiveView('settings')} onClick={() => setActiveView('settings')}
className={`sidebar-nav-item flex h-8 w-full items-center px-3.5 rounded-lg text-[12px] text-left cursor-default transition-colors ${ className="sidebar-nav-item sidebar-settings-button group flex w-full items-center text-[13px] text-left cursor-default font-medium transition-colors"
activeView === 'settings'
? 'bg-item-selected text-text-primary font-semibold'
: 'text-text-secondary hover:bg-item-hover hover:text-text-primary'
}`}
> >
<Settings className={`w-4 h-4 mr-2 ${activeView === 'settings' ? 'text-accent' : 'text-text-muted'}`} strokeWidth={activeView === 'settings' ? 2.25 : 1.8} /> <Settings className={`w-[18px] h-[18px] mr-3 shrink-0 ${activeView === 'settings' ? 'text-white' : 'text-text-muted'}`} strokeWidth={activeView === 'settings' ? 2.5 : 2} />
<span>Settings</span> <span>Settings</span>
</button> </button>
</div> </div>
+548 -161
View File
@@ -1,104 +1,77 @@
@import "tailwindcss"; @import "tailwindcss";
:root { :root {
/* Default/fallback Light */ /* Default/fallback Light (macOS Light Mode) */
--main-bg: 0 0% 98%; --main-bg: 0 0% 98%;
--sidebar-bg: 0 0% 100% / 0.6; --sidebar-bg: 0 0% 95% / 0.8;
--sidebar-glass: 0 0% 100% / 0.6; --sidebar-glass: 0 0% 95% / 0.8;
--border-color: 0 0% 88%; --border-color: 0 0% 88%;
--item-hover: 0 0% 0% / 0.05; --item-hover: 0 0% 0% / 0.05;
--item-selected: 211 100% 50% / 0.15; --item-selected: 211 100% 50%;
--accent-color: 211 100% 52%; --accent-color: 211 100% 50%;
--text-primary: 0 0% 15%; --text-primary: 0 0% 10%;
--text-secondary: 0 0% 35%; --text-secondary: 0 0% 40%;
--text-muted: 0 0% 55%; --text-muted: 0 0% 55%;
--bg-modal: 0 0% 100%; --bg-modal: 0 0% 100%;
--bg-input: 0 0% 95%; --bg-input: 0 0% 100%;
--border-modal: 0 0% 85%; --border-modal: 0 0% 85%;
--surface-raised: 0 0% 100%; --surface-raised: 0 0% 100%;
--surface-overlay: 0 0% 100% / 0.96; --surface-overlay: 0 0% 100% / 0.95;
--shadow-color: 220 30% 10% / 0.14; --shadow-color: 220 10% 20% / 0.1;
} }
.theme-light { .theme-light {
color-scheme: light; color-scheme: light;
--main-bg: 210 20% 98%; --main-bg: 0 0% 98%;
--sidebar-bg: 0 0% 100% / 0.7; --sidebar-bg: 0 0% 95% / 0.8;
--sidebar-glass: 0 0% 100% / 0.7; --sidebar-glass: 0 0% 95% / 0.8;
--border-color: 210 10% 88%; --border-color: 0 0% 88%;
--item-hover: 0 0% 0% / 0.05; --item-hover: 0 0% 0% / 0.05;
--item-selected: 220 90% 56% / 0.15; --item-selected: 211 100% 50%;
--accent-color: 220 90% 56%; --accent-color: 211 100% 50%;
--text-primary: 220 20% 15%; --text-primary: 0 0% 10%;
--text-secondary: 220 15% 40%; --text-secondary: 0 0% 40%;
--text-muted: 220 10% 55%; --text-muted: 0 0% 55%;
--bg-modal: 0 0% 100%; --bg-modal: 0 0% 100%;
--bg-input: 0 0% 0% / 0.04; --bg-input: 0 0% 100%;
--border-modal: 0 0% 85%; --border-modal: 0 0% 85%;
--surface-raised: 0 0% 100%; --surface-raised: 0 0% 100%;
--surface-overlay: 0 0% 100% / 0.96; --surface-overlay: 0 0% 100% / 0.95;
--shadow-color: 220 30% 10% / 0.14; --shadow-color: 220 10% 20% / 0.1;
} }
.theme-dark { .theme-dark, .theme-dracula, .theme-nord {
color-scheme: dark; color-scheme: dark;
--main-bg: 228 16% 8%;
--sidebar-bg: 228 14% 10%;
--sidebar-glass: 228 14% 10%;
--border-color: 220 16% 32% / 0.34;
--item-hover: 220 20% 96% / 0.055;
--item-selected: 220 94% 68% / 0.14;
--accent-color: 220 94% 68%;
--text-primary: 220 18% 94%;
--text-secondary: 220 10% 70%;
--text-muted: 220 8% 52%;
--bg-modal: 228 13% 12%;
--bg-input: 228 17% 7%;
--border-modal: 220 15% 38% / 0.46;
--surface-raised: 228 13% 11%;
--surface-overlay: 228 13% 13% / 0.985;
--shadow-color: 230 40% 2% / 0.52;
}
.theme-dracula { /* Original-like dark neutrals */
color-scheme: dark; --main-bg: 0 0% 10%;
--main-bg: 231 15% 12%; --sidebar-bg: 0 0% 13%;
--sidebar-bg: 231 15% 16% / 0.7; --sidebar-glass: 0 0% 13%;
--sidebar-glass: 231 15% 16% / 0.7;
--border-color: 0 0% 100% / 0.08;
--item-hover: 0 0% 100% / 0.05;
--item-selected: 326 100% 74% / 0.2;
--accent-color: 326 100% 74%;
--text-primary: 60 30% 96%;
--text-secondary: 225 27% 60%;
--text-muted: 225 20% 45%;
--bg-modal: 231 15% 16%;
--bg-input: 0 0% 0% / 0.4;
--border-modal: 0 0% 100% / 0.1;
--surface-raised: 231 15% 14%;
--surface-overlay: 231 15% 16% / 0.96;
--shadow-color: 231 30% 4% / 0.46;
}
.theme-nord { --surface-raised: 0 0% 12%;
color-scheme: dark; --surface-overlay: 0 0% 14%;
--main-bg: 220 16% 16%;
--sidebar-bg: 220 16% 20% / 0.7; --border-color: 0 0% 100% / 0.10;
--sidebar-glass: 220 16% 20% / 0.7; --border-modal: 0 0% 100% / 0.14;
--border-color: 0 0% 100% / 0.08;
--item-hover: 0 0% 100% / 0.05; --item-hover: 0 0% 100% / 0.07;
--item-selected: 193 43% 67% / 0.2; --item-selected: 211 100% 56%;
--accent-color: 193 43% 67%; --accent-color: 211 100% 56%;
--text-primary: 218 27% 92%;
--text-secondary: 213 20% 65%; --text-primary: 0 0% 91%;
--text-muted: 213 15% 50%; --text-secondary: 0 0% 75%;
--bg-modal: 220 16% 20%; --text-muted: 0 0% 50%;
--bg-input: 0 0% 0% / 0.4;
--border-modal: 0 0% 100% / 0.1; --bg-modal: 0 0% 13%;
--surface-raised: 220 16% 18%; --bg-input: 0 0% 16%;
--surface-overlay: 220 16% 20% / 0.96;
--shadow-color: 220 30% 5% / 0.42; --shadow-color: 0 0% 0% / 0.30;
--status-completed: 136 62% 48%;
--status-paused: 0 0% 56%;
--status-downloading: 211 100% 56%;
--status-failed: 0 62% 58%;
} }
@theme { @theme {
@@ -120,7 +93,8 @@
--color-surface-overlay: hsl(var(--surface-overlay)); --color-surface-overlay: hsl(var(--surface-overlay));
--color-bg-context-menu: hsl(var(--surface-overlay)); --color-bg-context-menu: hsl(var(--surface-overlay));
--font-sans: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Inter, Roboto, Helvetica, Arial, sans-serif; /* Apple System Fonts */
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
} }
@layer base { @layer base {
@@ -132,7 +106,7 @@
body { body {
margin: 0; margin: 0;
background-color: var(--color-main-bg); background-color: transparent;
color: var(--color-text-primary); color: var(--color-text-primary);
font-family: var(--font-sans); font-family: var(--font-sans);
overflow: hidden; overflow: hidden;
@@ -141,7 +115,7 @@
cursor: default; cursor: default;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
transition: background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), color 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: background-color 0.2s ease, color 0.2s ease;
} }
* { * {
@@ -172,13 +146,13 @@
} }
:focus-visible { :focus-visible {
outline: 2px solid hsl(var(--accent-color) / 0.72); outline: 2px solid hsl(var(--accent-color) / 0.5);
outline-offset: 2px; outline-offset: 2px;
} }
html[data-font-size="small"] { font-size: 12px; } html[data-font-size="small"] { font-size: 13px; }
html[data-font-size="standard"] { font-size: 13px; } html[data-font-size="standard"] { font-size: 14px; }
html[data-font-size="large"] { font-size: 15px; } html[data-font-size="large"] { font-size: 16px; }
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 6px; width: 6px;
@@ -189,99 +163,101 @@
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: var(--color-border-color); background: var(--color-border-color);
border-radius: 3px; border-radius: 6px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: var(--color-text-muted); background: var(--color-text-muted);
} }
::selection { ::selection {
background: hsl(var(--accent-color) / 0.28); background: hsl(var(--accent-color) / 0.3);
color: var(--color-text-primary); color: var(--color-text-primary);
} }
} }
@layer components { @layer components {
/* macOS Surfaces */
.app-surface { .app-surface {
background: hsl(var(--surface-raised)); background: hsl(var(--surface-raised));
border: 1px solid hsl(var(--border-color)); border: 1px solid hsl(var(--border-color));
box-shadow: box-shadow:
0 1px 0 hsl(0 0% 100% / 0.025) inset, inset 0 1px 0 hsl(0 0% 100% / 0.055),
0 8px 24px hsl(var(--shadow-color)); 0 18px 50px hsl(var(--shadow-color));
border-radius: 10px;
} }
.app-card { .app-card {
background: hsl(var(--bg-modal)); background: hsl(var(--surface-raised));
border: 1px solid hsl(var(--border-color)); border: 1px solid hsl(var(--border-color));
border-radius: 12px;
box-shadow: box-shadow:
0 1px 0 hsl(0 0% 100% / 0.02) inset, inset 0 1px 0 hsl(0 0% 100% / 0.055),
0 4px 14px hsl(var(--shadow-color)); 0 18px 50px hsl(var(--shadow-color));
border-radius: 10px;
transition: none;
} }
.app-control { .app-control {
min-height: 32px; min-height: 28px;
border: 1px solid hsl(var(--border-modal)); border: 1px solid hsl(var(--border-modal));
border-radius: 7px; border-radius: 6px;
background: hsl(var(--bg-input)); background: hsl(var(--bg-input));
color: hsl(var(--text-primary)); color: hsl(var(--text-primary));
transition: border-color 140ms ease, background-color 140ms ease, box-shadow 140ms ease; transition: border-color 100ms ease;
} }
.app-control:hover:not(:disabled) { .app-control:hover:not(:disabled) {
border-color: hsl(var(--text-muted) / 0.55); border-color: hsl(var(--text-muted));
} }
.app-control:focus { .app-control:focus {
border-color: hsl(var(--accent-color) / 0.7); border-color: var(--color-accent);
box-shadow: 0 0 0 3px hsl(var(--accent-color) / 0.12); box-shadow: 0 0 0 3px hsl(var(--accent-color) / 0.2);
outline: none; outline: none;
} }
.app-button { .app-button {
display: inline-flex; display: inline-flex;
min-height: 32px; min-height: 28px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 7px; gap: 6px;
border: 1px solid hsl(var(--border-modal)); border: 1px solid hsl(var(--border-modal));
border-radius: 7px; border-radius: 6px;
background: hsl(var(--bg-input)); background: hsl(var(--bg-input));
color: hsl(var(--text-primary)); color: hsl(var(--text-primary));
font-weight: 600; font-weight: 500;
transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease, transform 100ms ease; font-size: 13px;
transition: background-color 100ms ease;
box-shadow: 0 1px 1px hsl(var(--shadow-color));
} }
.app-button:hover:not(:disabled) { .app-button:hover:not(:disabled) {
border-color: hsl(var(--text-muted) / 0.48);
background: hsl(var(--item-hover)); background: hsl(var(--item-hover));
} }
.app-button:active:not(:disabled) { .app-button:active:not(:disabled) {
transform: translateY(1px); background: hsl(var(--border-color));
} }
.app-button-primary { .app-button-primary {
border-color: hsl(var(--accent-color) / 0.72); border-color: transparent;
background: hsl(var(--accent-color)); background: var(--color-accent);
color: white; color: white;
box-shadow: 0 5px 16px hsl(var(--accent-color) / 0.16);
} }
.app-button-primary:hover:not(:disabled) { .app-button-primary:hover:not(:disabled) {
border-color: hsl(var(--accent-color));
background: hsl(var(--accent-color) / 0.9); background: hsl(var(--accent-color) / 0.9);
} }
.app-icon-button { .app-icon-button {
display: inline-flex; display: inline-flex;
width: 32px; width: 28px;
height: 32px; height: 28px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 7px; border-radius: 6px;
color: hsl(var(--text-secondary)); color: hsl(var(--text-secondary));
transition: background-color 140ms ease, color 140ms ease, transform 100ms ease; transition: background-color 100ms ease;
} }
.app-icon-button:hover:not(:disabled) { .app-icon-button:hover:not(:disabled) {
@@ -290,111 +266,529 @@
} }
.app-icon-button:active:not(:disabled) { .app-icon-button:active:not(:disabled) {
transform: scale(0.96); background: hsl(var(--border-color));
} }
.app-modal-backdrop { .app-modal-backdrop {
background: hsl(0 0% 0% / 0.58); background: hsl(0 0% 0% / 0.2);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
animation: fade-in 150ms ease-out; animation: fade-in 150ms ease-out;
} }
.app-modal { .app-modal {
background: hsl(var(--surface-overlay)); background: hsl(var(--surface-overlay));
backdrop-filter: blur(40px);
-webkit-backdrop-filter: blur(40px);
border: 1px solid hsl(var(--border-modal)); border: 1px solid hsl(var(--border-modal));
border-radius: 14px; border-radius: 12px;
box-shadow: box-shadow: 0 20px 40px hsl(var(--shadow-color));
0 1px 0 hsl(0 0% 100% / 0.05) inset, animation: modal-in 150ms ease-out;
0 24px 80px hsl(var(--shadow-color));
animation: modal-in 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
} }
.app-toast { .app-toast {
border: 1px solid hsl(var(--border-modal)); border: 1px solid hsl(var(--border-modal));
border-radius: 999px; border-radius: 8px;
background: hsl(var(--surface-overlay)); background: hsl(var(--surface-overlay));
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
color: hsl(var(--text-primary)); color: hsl(var(--text-primary));
box-shadow: 0 12px 32px hsl(var(--shadow-color)); box-shadow: 0 4px 12px hsl(var(--shadow-color));
animation: toast-in 180ms ease-out; animation: toast-in 200ms ease-out;
} }
.app-shell { .app-shell {
background: hsl(var(--main-bg)); background: hsl(var(--main-bg));
} }
.app-sidebar { .app-sidebar-shell {
background: hsl(var(--sidebar-bg)); padding: 9px 0 9px 9px;
box-shadow: inset -1px 0 hsl(var(--border-color)); background: transparent;
} }
.app-sidebar-panel {
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
background: hsl(0 0% 13%);
border: 1px solid hsl(0 0% 100% / 0.11);
border-top-left-radius: 18px;
border-bottom-left-radius: 18px;
border-top-right-radius: 17px;
border-bottom-right-radius: 17px;
box-shadow:
inset 0 1px 0 hsl(0 0% 100% / 0.045),
2px 0 10px hsl(0 0% 0% / 0.20);
}
.app-workspace { .app-workspace {
background: hsl(var(--main-bg)); background: hsl(0 0% 11%);
} }
.app-statusbar { .app-statusbar {
background: hsl(var(--surface-raised)); height: 26px;
box-shadow: 0 1px 0 hsl(0 0% 100% / 0.02) inset; font-size: 10px;
background: hsl(0 0% 11%);
border-top: 1px solid hsl(var(--border-color));
box-shadow: none;
}
.sidebar-nav-item {
height: 31px;
border-radius: 8px;
padding: 0 12px;
margin-bottom: 2px;
font-size: 12px;
transition: background-color 100ms ease;
} }
.sidebar-nav-item[data-active="true"] { .sidebar-nav-item[data-active="true"] {
box-shadow: inset 0 0 0 1px hsl(var(--accent-color) / 0.16); background: hsl(var(--accent-color)) !important;
color: white !important;
box-shadow:
inset 0 1px 0 hsl(0 0% 100% / 0.12),
0 1px 0 hsl(0 0% 0% / 0.10);
}
.sidebar-nav-item[data-active="true"] svg {
color: white !important;
}
.sidebar-nav-item[data-active="true"] .sidebar-count {
color: white !important;
background: hsl(0 0% 100% / 0.16) !important;
}
.sidebar-nav-item:not([data-active="true"]):hover {
background: hsl(0 0% 100% / 0.07);
color: hsl(var(--text-primary));
}
.sidebar-nav-item svg {
width: 15px;
height: 15px;
}
.sidebar-inner {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.sidebar-top-region {
height: 40px;
flex-shrink: 0;
position: relative;
}
.sidebar-scroll {
flex: 1;
overflow-y: auto;
padding: 6px 10px 10px;
-ms-overflow-style: none;
scrollbar-width: none;
}
.sidebar-scroll::-webkit-scrollbar {
display: none;
}
.sidebar-footer {
flex-shrink: 0;
padding: 7px 10px;
border-top: 1px solid hsl(0 0% 100% / 0.08);
background: hsl(0 0% 100% / 0.012);
}
.sidebar-settings-button {
height: 31px;
border-radius: 8px;
font-size: 12px;
margin-bottom: 0;
}
.sidebar-section-label {
padding: 0 12px;
margin-bottom: 7px;
font-size: 9px;
line-height: 1;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
color: hsl(var(--text-muted));
}
.sidebar-section {
margin-bottom: 14px;
}
.sidebar-toggle-button {
position: absolute;
top: 11px;
right: 12px;
z-index: 50;
width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 6px;
color: hsl(var(--text-secondary));
background: transparent;
}
.sidebar-toggle-button:hover {
background: hsl(0 0% 100% / 0.07);
color: hsl(var(--text-primary));
} }
.settings-view, .settings-view,
.settings-scroll { .settings-scroll {
background: hsl(var(--main-bg)); background: transparent;
} }
.settings-toolbar { .settings-toolbar {
background: hsl(var(--sidebar-bg)); background: hsl(var(--sidebar-bg));
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid hsl(var(--border-color)); border-bottom: 1px solid hsl(var(--border-color));
} }
.settings-tab-strip { .settings-tab-strip {
background: hsl(var(--bg-input)); background: hsl(var(--bg-input));
border: 1px solid hsl(var(--border-color)); border: 1px solid hsl(var(--border-color));
box-shadow: 0 1px 0 hsl(0 0% 100% / 0.02) inset; border-radius: 8px;
padding: 2px;
}
.settings-tab-button {
border-radius: 6px;
transition: none;
} }
.settings-tab-button[data-active="true"] { .settings-tab-button[data-active="true"] {
background: hsl(var(--item-selected)); background: hsl(var(--surface-raised));
box-shadow: inset 0 0 0 1px hsl(var(--accent-color) / 0.14); box-shadow: 0 1px 2px hsl(0 0% 0% / 0.1);
color: var(--color-text-primary);
} }
.settings-pane { .settings-pane {
padding: 22px; padding: 24px;
background: transparent;
}
.mac-settings-group {
background: hsl(var(--bg-modal));
border-radius: 10px;
border: 1px solid hsl(var(--border-color)); border: 1px solid hsl(var(--border-color));
border-radius: 12px; box-shadow: 0 1px 2px hsl(var(--shadow-color));
background: hsl(var(--surface-raised)); overflow: hidden;
box-shadow: margin-bottom: 24px;
0 1px 0 hsl(0 0% 100% / 0.02) inset, }
0 5px 18px hsl(var(--shadow-color));
.mac-settings-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid hsl(var(--border-color));
}
.mac-settings-row:last-child {
border-bottom: none;
}
/* macOS Switch */
.mac-switch {
appearance: none;
width: 36px;
height: 20px;
background: hsl(var(--border-color));
border-radius: 20px;
position: relative;
cursor: default;
outline: none;
transition: background 0.2s ease;
}
.mac-switch::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background: white;
border-radius: 50%;
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
transition: transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.mac-switch:checked {
background: var(--color-accent);
}
.mac-switch:checked::after {
transform: translateX(16px);
}
.downloads-view {
background: hsl(var(--main-bg));
}
.main-titlebar {
height: 52px;
display: flex;
align-items: center;
padding: 0 18px;
border-bottom: 1px solid hsl(var(--border-color));
background: hsl(0 0% 11%);
}
.main-titlebar-title {
font-size: 14px;
font-weight: 700;
color: hsl(var(--text-primary));
letter-spacing: -0.01em;
}
.main-control-group {
margin-left: auto;
height: 28px;
display: inline-flex;
align-items: center;
overflow: hidden;
border-radius: 15px;
border: 1px solid hsl(0 0% 100% / 0.12);
background: hsl(0 0% 16%);
}
.main-control-button {
width: 30px;
height: 26px;
display: inline-flex;
align-items: center;
justify-content: center;
color: hsl(var(--text-secondary));
border-right: 1px solid hsl(0 0% 100% / 0.08);
}
.main-control-button svg {
width: 14px;
height: 14px;
}
.main-control-button:last-child {
border-right: 0;
}
.main-control-button:hover:not(:disabled) {
background: hsl(0 0% 100% / 0.07);
color: hsl(var(--text-primary));
}
.main-control-button.primary {
color: hsl(var(--accent-color));
}
.main-control-button:disabled {
opacity: 0.38;
}
.downloads-content-header {
height: 56px;
display: flex;
align-items: center;
padding: 0 18px;
background: hsl(var(--main-bg));
}
.downloads-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
line-height: 1;
font-weight: 700;
color: hsl(var(--text-primary));
letter-spacing: -0.02em;
}
.downloads-count {
min-width: 20px;
height: 20px;
padding: 0 6px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
color: hsl(var(--text-secondary));
background: hsl(0 0% 100% / 0.08);
}
.downloads-table {
flex: 1;
min-height: 0;
overflow: hidden;
background: hsl(var(--main-bg));
}
.download-table-header {
height: 34px;
display: grid;
grid-template-columns: minmax(260px, 1fr) 132px 220px 126px 100px 190px;
align-items: center;
padding: 0 18px;
border-top: 1px solid hsl(var(--border-color));
border-bottom: 1px solid hsl(var(--border-color));
color: hsl(var(--text-secondary));
font-size: 12px;
font-weight: 700;
}
.download-table-body {
height: 100%;
overflow: hidden;
}
.download-row {
height: 34px;
display: grid;
grid-template-columns: minmax(260px, 1fr) 132px 220px 126px 100px 190px;
align-items: center;
padding: 0 18px;
border-bottom: 1px solid hsl(0 0% 100% / 0.055);
color: hsl(var(--text-secondary));
font-size: 12px;
}
.download-row:nth-child(even) {
background: hsl(0 0% 100% / 0.025);
}
.download-row:hover {
background: hsl(0 0% 100% / 0.055);
}
.download-file-cell {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.download-file-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: hsl(var(--text-primary));
font-weight: 600;
}
.download-cell-muted {
color: hsl(var(--text-muted));
}
.download-cell-right {
text-align: right;
display: flex;
justify-content: flex-end;
align-items: center;
}
.download-empty-row {
color: hsl(var(--text-muted));
}
.download-empty-row .download-file-name {
color: hsl(var(--text-muted));
font-weight: 500;
}
.download-ghost-row {
height: 34px;
border-bottom: 1px solid hsl(0 0% 100% / 0.045);
}
.download-ghost-row:nth-child(even) {
background: hsl(0 0% 100% / 0.025);
}
.download-progress-status {
display: flex;
align-items: center;
gap: 10px;
}
.download-progress-track {
width: 128px;
height: 7px;
border-radius: 999px;
overflow: hidden;
background: hsl(0 0% 100% / 0.12);
box-shadow: inset 0 1px 1px hsl(0 0% 0% / 0.35);
}
.download-progress-fill {
height: 100%;
border-radius: inherit;
background: hsl(var(--status-downloading));
}
.download-progress-fill.paused {
background: hsl(28 95% 56%);
}
.download-status-paused {
color: hsl(var(--status-paused));
font-weight: 600;
}
.download-status-completed {
color: hsl(var(--status-completed));
font-weight: 700;
}
.download-status-failed {
color: hsl(var(--status-failed));
font-weight: 700;
}
.download-status-downloading {
color: hsl(var(--status-downloading));
font-weight: 600;
} }
} }
.glass-panel { .glass-panel {
background: var(--color-sidebar-glass); background: var(--color-sidebar-glass);
backdrop-filter: blur(30px);
-webkit-backdrop-filter: blur(30px);
} }
.glass-card { .glass-card {
background: hsl(var(--bg-modal)); background: hsl(var(--bg-modal) / 0.8);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--color-border-color); border: 1px solid var(--color-border-color);
box-shadow: 0 6px 20px hsl(var(--shadow-color)); box-shadow: 0 4px 12px hsl(var(--shadow-color));
} border-radius: 8px;
/* Base App Background Gradient - Removed for Modern Minimalist Look */
.app-bg-gradient {
background: transparent;
} }
.settings-tab-label { .settings-tab-label {
font-size: 11px; font-size: 13px;
font-weight: 500;
} }
@media (max-width: 900px) { @media (max-width: 900px) {
.settings-tab-label { .settings-tab-label {
font-size: 9px; font-size: 11px;
letter-spacing: -0.01em; letter-spacing: -0.01em;
} }
} }
@@ -405,22 +799,15 @@
} }
@keyframes modal-in { @keyframes modal-in {
from { opacity: 0; transform: translateY(6px) scale(0.985); } from { opacity: 0; transform: scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); } to { opacity: 1; transform: scale(1); }
} }
@keyframes toast-in { @keyframes toast-in {
from { opacity: 0; transform: translate(-50%, 6px); } from { opacity: 0; transform: translate(-50%, 8px); }
to { opacity: 1; transform: translate(-50%, 0); } to { opacity: 1; transform: translate(-50%, 0); }
} }
.hover-lift {
transition: transform 120ms ease, background-color 140ms ease, border-color 140ms ease;
}
.hover-lift:hover {
transform: translateY(-1px);
}
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
*, *,
*::before, *::before,
+1 -1
View File
@@ -24,7 +24,7 @@ const tauriStorage: StateStorage = {
} }
} }
}, },
removeItem: async (name: string): Promise<void> => { removeItem: async (_name: string): Promise<void> => {
// no-op for now // no-op for now
}, },
}; };