style(ui): overhaul background and themes for modern minimalist aesthetics

This commit is contained in:
NimBold
2026-06-13 14:45:59 +03:30
parent f6c7a0b8d2
commit ba37d78cba
69 changed files with 369 additions and 253 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

+36
View File
@@ -334,6 +334,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.11.1"
@@ -1826,6 +1832,19 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "image"
version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [
"bytemuck",
"byteorder-lite",
"moxcms",
"num-traits",
"png 0.18.1",
]
[[package]]
name = "indexmap"
version = "1.9.3"
@@ -2166,6 +2185,16 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "moxcms"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "muda"
version = "0.19.2"
@@ -2895,6 +2924,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "pxfm"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
[[package]]
name = "quick-xml"
version = "0.37.5"
@@ -3856,6 +3891,7 @@ dependencies = [
"gtk",
"heck 0.5.0",
"http",
"image",
"jni",
"libc",
"log",
+1 -1
View File
@@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = ["tray-icon"] }
tauri = { version = "2", features = ["tray-icon", "image-png"] }
tauri-plugin-opener = "2"
tauri-plugin-dialog = "2"
serde = { version = "1", features = ["derive"] }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

+32 -62
View File
@@ -697,11 +697,13 @@ async fn start_media_download(
password: Option<String>,
headers: Option<String>,
proxy: Option<String>,
user_agent: Option<String>,
max_tries: Option<i32>,
) -> Result<(), String> {
let task = queue::DownloadTask {
is_media: true,
id, url, destination, filename, speed_limit, username, password, headers, proxy,
connections: None, checksum: None, cookies: None, mirrors: None, user_agent: None, max_tries: None,
connections: None, checksum: None, cookies: None, mirrors: None, user_agent, max_tries,
format_selector, cookie_source,
};
state.cmd_tx.send(queue::DownloadCommand::Enqueue(task)).map_err(|e| e.to_string())?;
@@ -725,6 +727,8 @@ pub(crate) async fn start_media_download_internal(
let password = task.password;
let headers = task.headers;
let proxy = task.proxy;
let user_agent = task.user_agent;
let max_tries = task.max_tries;
println!("start_media_download called for id: {}", id);
let resource_dir = app_handle.path().resource_dir().map_err(|e| e.to_string())?;
@@ -779,12 +783,23 @@ pub(crate) async fn start_media_download_internal(
}
}
if let Some(cs) = cookie_source {
if let Some(mut cs) = cookie_source {
if !cs.is_empty() && cs != "none" {
if cs == "safari" { cs = "safari:".to_string() }
cmd.arg("--cookies-from-browser").arg(cs);
}
}
if let Some(ua) = user_agent {
if !ua.is_empty() {
cmd.arg("--user-agent").arg(ua);
}
}
if let Some(tries) = max_tries {
cmd.arg("--retries").arg(tries.to_string());
}
let mut config_file = tempfile::Builder::new().prefix("ytdlp-").suffix(".conf").tempfile().map_err(|e| e.to_string())?;
let mut config_content = String::new();
if let Some(user) = username {
@@ -998,70 +1013,23 @@ fn set_prevent_sleep(state: tauri::State<'_, AppState>, prevent: bool) {
#[tauri::command]
fn perform_system_action(action: String) -> Result<(), String> {
let status = match action.as_str() {
"sleep" => {
#[cfg(target_os = "macos")]
{
Command::new("osascript")
.arg("-e")
.arg("tell application \"Finder\" to sleep")
.status()
}
#[cfg(target_os = "windows")]
{
Command::new("rundll32.exe")
.arg("powrprof.dll,SetSuspendState")
.arg("0,1,0")
.status()
}
#[cfg(target_os = "linux")]
{
Command::new("systemctl").arg("suspend").status()
}
}
"restart" => {
#[cfg(target_os = "macos")]
{
Command::new("osascript")
.arg("-e")
.arg("tell application \"Finder\" to restart")
.status()
}
#[cfg(target_os = "windows")]
{
Command::new("shutdown").args(["/r", "/t", "0"]).status()
}
#[cfg(target_os = "linux")]
{
Command::new("systemctl").arg("reboot").status()
}
}
"shutdown" => {
#[cfg(target_os = "macos")]
{
Command::new("osascript")
.arg("-e")
.arg("tell application \"Finder\" to shut down")
.status()
}
#[cfg(target_os = "windows")]
{
Command::new("shutdown").args(["/s", "/t", "0"]).status()
}
#[cfg(target_os = "linux")]
{
Command::new("systemctl").arg("poweroff").status()
}
}
_ => return Err("Unsupported system action".to_string()),
"shutdown" => std::process::Command::new("osascript").arg("-e").arg("tell app \"System Events\" to shut down").status(),
"restart" => std::process::Command::new("osascript").arg("-e").arg("tell app \"System Events\" to restart").status(),
"sleep" => std::process::Command::new("osascript").arg("-e").arg("tell app \"System Events\" to sleep").status(),
_ => return Err("Invalid action".to_string())
};
match status {
Ok(result) if result.success() => Ok(()),
Ok(result) => Err(format!("System action exited with status {result}")),
Err(error) => Err(format!("Failed to perform system action: {error}")),
Ok(_) => Ok(()),
Err(e) => Err(e.to_string())
}
}
#[tauri::command]
fn set_concurrent_limit(state: tauri::State<'_, AppState>, limit: usize) {
let _ = state.cmd_tx.send(queue::DownloadCommand::SetLimit(limit));
}
#[tauri::command]
fn request_automation_permission() -> Result<(), String> {
#[cfg(target_os = "macos")]
@@ -1216,8 +1184,10 @@ fn toggle_tray_icon(app_handle: tauri::AppHandle, show: bool) -> Result<(), Stri
let show_i = MenuItem::with_id(&app_handle, "show", "Show Firelink", true, None::<&str>).map_err(|e| e.to_string())?;
let menu = Menu::with_items(&app_handle, &[&show_i, &quit_i]).map_err(|e| e.to_string())?;
let tray_icon = tauri::image::Image::from_bytes(include_bytes!("../icons/trayTemplate.png")).unwrap();
let _tray = TrayIconBuilder::with_id("main")
.icon(app_handle.default_window_icon().unwrap().clone())
.icon(tray_icon)
.icon_as_template(true)
.menu(&menu)
.on_menu_event(|app, event| match event.id.as_ref() {
"quit" => {
@@ -1342,7 +1312,7 @@ pub fn run() {
request_automation_permission, open_automation_settings,
set_keychain_password, get_keychain_password, delete_keychain_password,
check_file_exists, delete_file, toggle_tray_icon, set_extension_pairing_token,
set_extension_frontend_ready,
set_extension_frontend_ready, set_concurrent_limit,
parity::get_system_proxy, parity::get_file_category, parity::check_for_updates
])
.run(tauri::generate_context!())
+1 -1
View File
@@ -30,7 +30,7 @@ pub enum DownloadCommand {
}
pub fn setup_queue(app_handle: tauri::AppHandle, mut cmd_rx: mpsc::UnboundedReceiver<DownloadCommand>) {
tokio::spawn(async move {
tauri::async_runtime::spawn(async move {
let mut queue = std::collections::VecDeque::new();
let mut active_count = 0;
let mut limit = 3;
+28 -10
View File
@@ -53,11 +53,16 @@ function App() {
const schedulerRunning = useSettingsStore(state => state.schedulerRunning);
const globalSpeedLimit = useSettingsStore(state => state.globalSpeedLimit);
const previousSpeedLimit = useRef(globalSpeedLimit);
const maxConcurrentDownloads = useSettingsStore(state => state.maxConcurrentDownloads);
useEffect(() => {
window.document.documentElement.setAttribute('data-font-size', appFontSize);
}, [appFontSize]);
useEffect(() => {
invoke('set_concurrent_limit', { limit: maxConcurrentDownloads }).catch(console.error);
}, [maxConcurrentDownloads]);
useEffect(() => {
invoke('update_dock_badge', { count: showDockBadge ? activeDownloadCount : 0 }).catch(() => {});
}, [showDockBadge, activeDownloadCount]);
@@ -231,14 +236,18 @@ function App() {
}, []);
return (
<div className="flex h-screen w-screen bg-main-bg text-text-primary overflow-hidden">
<div className="flex h-screen w-screen bg-main-bg app-bg-gradient text-text-primary overflow-hidden">
{/* 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">
<div
className={`flex flex-col overflow-hidden relative z-20 shrink-0 transition-all duration-300 ease-in-out glass-panel ${
isSidebarVisible ? 'w-[240px] opacity-100 shadow-[4px_0_24px_rgba(0,0,0,0.2)] border-r border-border-color/30' : 'w-0 opacity-0 border-r-0'
}`}
>
<div className="w-[240px] h-full flex flex-col shrink-0">
<Sidebar selectedFilter={filter} onSelectFilter={(f) => { setFilter(f); useSettingsStore.getState().setActiveView('downloads'); }} />
</div>
)}
</div>
{/* Main Content - Base Layer */}
<div className="flex-1 flex flex-col h-full relative z-0">
@@ -250,12 +259,21 @@ function App() {
</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 className="h-8 px-6 flex items-center justify-between text-[11px] text-text-muted font-medium shrink-0 border-t border-border-color/20 glass-panel">
<span className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-accent/80 animate-pulse"></span>
Ready
</span>
<div className="flex gap-4">
<span className="flex items-center gap-1.5">
<span className="text-text-primary">{activeDownloadCount}</span> active
</span>
<span className="flex items-center gap-1.5">
<span className="text-text-primary">{queuedCount}</span> queued
</span>
<span className="flex items-center gap-1.5">
<span className="text-text-primary">{doneCount}</span> done
</span>
</div>
</div>
</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

+66 -75
View File
@@ -13,16 +13,7 @@ interface DownloadTableProps {
export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
const { downloads, toggleAddModal, updateDownload, removeDownload, clearFinished, redownload } = useDownloadStore();
const { isSidebarVisible, toggleSidebar, listRowDensity } = useSettingsStore();
const getPaddingY = () => {
switch (listRowDensity) {
case 'compact': return 'py-1';
case 'relaxed': return 'py-4';
default: return 'py-3';
}
};
const py = getPaddingY();
const { isSidebarVisible, toggleSidebar } = useSettingsStore();
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null);
@@ -111,34 +102,35 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
}
return (
<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' : ''} />
<div className="flex-1 flex flex-col bg-transparent h-full relative p-4 pb-0">
<div className="glass-panel shrink-0 rounded-[16px] mb-3 shadow-sm border border-border-color/40 z-10">
<WindowDragRegion className={!isSidebarVisible ? 'pl-20' : ''} />
{/* Download Toolbar */}
<div className={`flex px-6 pb-4 items-center ${!isSidebarVisible ? 'pl-24' : ''}`}>
<div className={`flex px-5 pb-3 pt-1 items-center ${!isSidebarVisible ? 'pl-20' : ''}`}>
<button
onClick={toggleSidebar}
className="mr-4 p-1.5 rounded-md text-text-secondary hover:text-text-primary hover:bg-item-hover transition-colors"
className="mr-4 p-1.5 rounded-lg text-text-secondary hover:text-text-primary hover:bg-item-hover transition-all duration-200 hover-lift"
title="Toggle Sidebar"
>
<PanelLeft size={18} strokeWidth={2} />
<PanelLeft size={18} strokeWidth={2.5} />
</button>
<h2 className="text-[15px] font-bold mr-auto text-text-primary tracking-tight cursor-default">{getFilterTitle()}</h2>
<div className="flex gap-1.5 items-center">
<div className="flex gap-2 items-center bg-item-hover/30 p-1 rounded-xl border border-border-color/30">
<button
onClick={() => toggleAddModal(true)}
className="p-1.5 rounded-md text-text-secondary hover:text-white hover:bg-item-hover transition-all duration-200"
className="p-1.5 rounded-lg text-text-secondary hover:text-white hover:bg-accent hover:shadow-md hover:shadow-accent/30 transition-all duration-200 hover-lift"
title="Add Download"
>
<Plus size={16} strokeWidth={2} />
<Plus size={16} strokeWidth={2.5} />
</button>
<div className="w-[1px] h-4 bg-border-color/50"></div>
<button
onClick={() => {
filteredDownloads.filter(d => d.status === 'paused').forEach(d => handleResume(d));
}}
className="p-1.5 rounded-md text-text-secondary hover:text-white hover:bg-item-hover transition-all duration-200"
className="p-1.5 rounded-lg text-text-secondary hover:text-white hover:bg-green-500 hover:shadow-md hover:shadow-green-500/30 transition-all duration-200 hover-lift"
title="Resume All"
>
<Play size={16} fill="currentColor" className="opacity-90" />
@@ -147,50 +139,49 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
onClick={() => {
filteredDownloads.filter(d => d.status === 'downloading').forEach(d => handlePause(d.id));
}}
className="p-1.5 rounded-md text-text-secondary hover:text-white hover:bg-item-hover transition-all duration-200"
className="p-1.5 rounded-lg text-text-secondary hover:text-white hover:bg-orange-500 hover:shadow-md hover:shadow-orange-500/30 transition-all duration-200 hover-lift"
title="Pause All"
>
<Pause size={16} fill="currentColor" className="opacity-90" />
</button>
<div className="w-[1px] h-4 bg-border-color/50"></div>
<button
onClick={clearFinished}
className="p-1.5 rounded-md text-text-secondary hover:text-red-400 hover:bg-item-hover transition-all duration-200"
className="p-1.5 rounded-lg text-text-secondary hover:text-white hover:bg-red-500 hover:shadow-md hover:shadow-red-500/30 transition-all duration-200 hover-lift"
title="Clear Finished"
>
<Trash2 size={16} />
<Trash2 size={16} strokeWidth={2} />
</button>
</div>
</div>
</div>
{/* Table */}
<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">
<th className={`${py} px-3 pl-8`}>FILE</th>
<th className={`${py} px-3 w-40`}>SIZE</th>
<th className={`${py} px-3 w-36`}>STATUS</th>
<th className={`${py} px-3 w-28`}>SPEED</th>
<th className={`${py} px-3 w-28`}>ETA</th>
<th className={`${py} px-3 pr-8 w-36`}>DATE ADDED</th>
</tr>
</thead>
<tbody>
{/* List */}
<div className="flex-1 overflow-auto bg-transparent px-1 pb-4 relative">
<div className="w-full text-left">
<div className="flex text-text-muted/60 text-[10px] font-bold tracking-widest uppercase px-4 pb-2 pt-2 sticky top-0 z-0 bg-transparent backdrop-blur-md">
<div className="flex-1 min-w-[200px]">FILE</div>
<div className="w-32">SIZE</div>
<div className="w-32">STATUS</div>
<div className="w-28">SPEED</div>
<div className="w-28">ETA</div>
<div className="w-32 text-right pr-4">DATE ADDED</div>
</div>
<div className="flex flex-col gap-2">
{filteredDownloads.length === 0 ? (
<tr>
<td colSpan={6} className="p-16 text-center">
<div className="flex flex-col items-center justify-center text-text-muted/50 gap-3">
<Box size={48} strokeWidth={1} />
<span className="text-sm font-medium">No downloads in this view</span>
<div className="p-16 text-center glass-card rounded-[16px] mx-2 mt-4">
<div className="flex flex-col items-center justify-center text-text-muted/50 gap-4">
<div className="p-4 bg-item-hover rounded-full animate-float">
<Box size={40} strokeWidth={1.5} />
</div>
</td>
</tr>
<span className="text-[13px] font-medium tracking-wide">No downloads in this view</span>
</div>
</div>
) : (
filteredDownloads.map(d => (
<tr
<div
key={d.id}
className="border-b border-border-color/30 hover:bg-item-hover transition-colors duration-200 group cursor-default"
className="flex items-center px-4 py-3 bg-item-hover/20 hover:bg-item-hover/50 border border-border-color/20 hover:border-border-color/40 rounded-[14px] transition-all duration-200 group cursor-default hover-lift shadow-sm mx-1"
onContextMenu={(e) => {
e.preventDefault();
setContextMenu({
@@ -200,19 +191,19 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
});
}}
>
<td className={`${py} px-3 pl-8 text-[13px] text-text-primary`}>
<div className="flex-1 min-w-[200px] text-[13px] text-text-primary pr-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-item-hover rounded-md border border-border-color/50 text-text-muted">
<div className="p-2.5 bg-bg-modal shadow-sm rounded-xl border border-border-color/40 text-text-muted group-hover:scale-105 transition-transform">
{getCategoryIcon(d.category)}
</div>
<span className="font-medium truncate max-w-[280px]">{d.fileName}</span>
</div>
</td>
<td className={`${py} px-3`}>
</div>
<div className="w-32 pr-4">
{d.status === 'downloading' || d.status === 'paused' ? (
<div className="w-full pr-4">
<div className="w-full bg-[#3f3f3f] rounded-full h-1.5 mb-1.5 overflow-hidden">
<div className={`h-1.5 rounded-full transition-all duration-300 ${d.status === 'paused' ? 'bg-orange-500' : 'bg-[#3B66DE]'}`} style={{ width: `${(d.fraction || 0) * 100}%` }}></div>
<div 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)}%
@@ -221,33 +212,33 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
) : (
<span className="text-[12px] text-text-secondary font-medium">{d.size || '-'}</span>
)}
</td>
<td className={`${py} px-3`}>
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold tracking-widest uppercase ${
d.status === 'completed' ? 'text-[#4CAF50]' :
d.status === 'downloading' ? 'bg-[#1E3A8A] text-[#60A5FA]' :
d.status === 'failed' ? 'text-red-400' :
d.status === 'paused' ? 'text-orange-400' :
'text-text-muted'
</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>
</td>
<td className={`${py} px-3 text-[12px] text-text-secondary font-medium`}>{d.speed}</td>
<td className={`${py} px-3 text-[12px] text-text-secondary font-medium`}>{d.eta}</td>
<td className={`${py} px-3 pr-8 relative`}>
<div className="flex items-center justify-between">
<span className="text-[12px] text-text-secondary font-medium">
</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 className="w-32 relative text-right pr-4">
<div className="flex items-center justify-end">
<span className="text-[12px] text-text-secondary font-medium group-hover:opacity-0 transition-opacity duration-200">
{d.dateAdded ? new Date(d.dateAdded).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) : '-'}
</span>
<div className="flex justify-end gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity duration-200 absolute right-8 top-1/2 -translate-y-1/2">
<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">
{d.status === 'downloading' && (
<button onClick={() => handlePause(d.id)} className="p-1 bg-[#2A2B2D] border border-[#3f3f3f] hover:bg-[#3f3f3f] rounded text-orange-400 transition-colors" title="Pause">
<button onClick={() => handlePause(d.id)} className="p-1.5 bg-bg-modal border border-border-color shadow-sm hover:border-orange-500 hover:text-orange-500 rounded-lg text-text-muted transition-all hover-lift" title="Pause">
<Pause size={14} fill="currentColor" />
</button>
)}
{d.status === 'paused' && (
<button onClick={() => handleResume(d)} className="p-1 bg-[#2A2B2D] border border-[#3f3f3f] hover:bg-[#3f3f3f] rounded text-green-400 transition-colors" title="Resume">
<button onClick={() => handleResume(d)} className="p-1.5 bg-bg-modal border border-border-color shadow-sm hover:border-green-500 hover:text-green-500 rounded-lg text-text-muted transition-all hover-lift" title="Resume">
<Play size={14} fill="currentColor" />
</button>
)}
@@ -256,18 +247,18 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
e.stopPropagation();
setContextMenu({ x: e.clientX, y: e.clientY, id: d.id });
}}
className="p-1 bg-[#2A2B2D] border border-[#3f3f3f] hover:bg-[#3f3f3f] rounded text-text-muted hover:text-text-primary transition-colors"
className="p-1.5 bg-bg-modal border border-border-color shadow-sm hover:border-accent hover:text-accent rounded-lg text-text-muted transition-all hover-lift"
>
<MoreVertical size={14} />
</button>
</div>
</div>
</td>
</tr>
</div>
</div>
))
)}
</tbody>
</table>
</div>
</div>
</div>
{/* Floating Context Menu */}
+65 -16
View File
@@ -2,11 +2,12 @@ import { useState, useEffect } from 'react';
import { SettingsTab, useSettingsStore } from '../store/useSettingsStore';
import {
Download, Palette, Globe, Folder, Key,
Moon, Terminal, Puzzle, Info, Plus, Trash2, Copy, RefreshCw
Moon, Terminal, Puzzle, Info, Plus, Trash2, Copy, RefreshCw, Code
} from 'lucide-react';
import { open } from '@tauri-apps/plugin-dialog';
import { invoke } from '@tauri-apps/api/core';
import { WindowDragRegion } from './WindowDragRegion';
import appIcon from '../assets/app-icon.png';
const settingsTabs: { type: SettingsTab; label: string; icon: typeof Download }[] = [
{ type: 'downloads', label: 'Downloads', icon: Download },
@@ -793,24 +794,72 @@ export default function SettingsView() {
{/* About Pane */}
{activeTab === 'about' && (
<div className="space-y-6 max-w-md mx-auto text-center py-6">
<div className="w-16 h-16 bg-accent text-white font-extrabold text-2xl flex items-center justify-center rounded-2xl mx-auto shadow-lg shadow-accent/20">
FL
</div>
<div className="space-y-2">
<h3 className="text-lg font-bold text-text-primary">Firelink Desktop</h3>
<p className="text-text-secondary text-sm">Version 0.8.0-rewrite (Tauri v2)</p>
<p className="text-text-muted text-xs leading-relaxed max-w-sm mx-auto">
A high-speed, cross-platform download engine rebuilt in Rust, React, and Tailwind, replicating the premium SwiftUI native look.
</p>
<div className="space-y-6 max-w-[720px]">
{/* Header Box */}
<div className="bg-bg-modal border border-border-modal/40 rounded-xl p-6 flex items-center gap-4">
<img src={appIcon} alt="Firelink Icon" className="w-[72px] h-[72px] drop-shadow-md rounded-xl" />
<div className="space-y-1">
<h3 className="text-[17px] font-bold text-text-primary">Firelink</h3>
<p className="text-text-secondary text-[12px] font-medium">Version 0.7.3</p>
<p className="text-text-muted text-[11px]">
A native macOS download manager for fast, organized, segmented transfers.
</p>
</div>
</div>
<div className="border-t border-border-color/30 pt-4 flex justify-center gap-4 text-xs text-accent">
<a href="https://github.com/nimbold/Firelink" target="_blank" rel="noreferrer" className="hover:underline">GitHub Repository</a>
<span></span>
<a href="https://github.com/nimbold/Firelink/issues" target="_blank" rel="noreferrer" className="hover:underline">Report Issues</a>
{/* Updates Section */}
<div className="space-y-2">
<h4 className="text-[12px] font-bold text-text-primary px-1">Updates</h4>
<div className="bg-bg-modal border border-border-modal/40 rounded-xl overflow-hidden">
<div className="p-4 flex items-center justify-between border-b border-border-modal/40">
<div>
<p className="text-[13px] font-bold text-text-primary">Check for Updates</p>
<p className="text-text-muted text-[11px] mt-0.5">Firelink checks GitHub Releases for new versions.</p>
</div>
<button
onClick={() => {
showToast("Checking for updates...");
invoke('check_for_updates').then((res: any) => {
if (res === "Up to date.") showToast("Firelink is up to date");
else showToast(res);
}).catch((e) => showToast(String(e)));
}}
className="bg-item-hover hover:bg-item-hover/80 text-text-primary px-4 py-1.5 rounded-lg text-xs font-semibold border border-border-modal/50 transition-colors"
>
Check Now
</button>
</div>
<label className="p-4 flex items-center justify-between cursor-default">
<span className="text-[13px] font-bold text-text-primary">Automatically check for updates</span>
<button
type="button"
role="switch"
aria-checked={settings.autoCheckUpdates}
onClick={() => settings.setAutoCheckUpdates(!settings.autoCheckUpdates)}
className={`relative inline-flex h-5 w-9 shrink-0 cursor-default items-center rounded-full transition-colors duration-200 ease-in-out border border-transparent ${settings.autoCheckUpdates ? 'bg-accent' : 'bg-border-color'}`}
>
<span className={`pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow transition duration-200 ease-in-out ${settings.autoCheckUpdates ? 'translate-x-4' : 'translate-x-0'}`} />
</button>
</label>
</div>
</div>
{/* Credits Footer */}
<div className="bg-bg-modal border border-border-modal/40 rounded-xl p-4 text-[11px] space-y-3">
<div className="flex justify-between items-center">
<span className="text-text-primary font-bold">Created by NimBold</span>
<a href="https://github.com/nimbold/Firelink" target="_blank" rel="noreferrer" className="flex items-center gap-1.5 text-text-secondary hover:text-accent transition-colors font-medium">
<Code size={14} /> Source Code
</a>
</div>
<div className="flex justify-between items-center text-text-muted">
<span>Powered by <span className="text-accent">aria2</span> <span className="text-accent">yt-dlp</span> <span className="text-accent">ffmpeg</span> <span className="text-accent">Deno</span></span>
<a href="https://github.com/nimbold/Firelink/blob/main/LICENSE" target="_blank" rel="noreferrer" className="text-accent hover:underline">MIT License</a>
</div>
<div className="text-text-muted pt-1 border-t border-border-modal/40">
Copyright © 2026 NimBold. All rights reserved.
</div>
</div>
<p className="text-[10px] text-text-muted pt-2">© 2026 Firelink Project. Released under the MIT License.</p>
</div>
)}
+24 -24
View File
@@ -63,18 +63,18 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
return (
<button
type="button"
className={`group flex w-full items-center px-2.5 py-1.5 rounded-lg text-[13px] text-left cursor-default transition-colors mb-0.5 ${
className={`group flex w-full items-center px-3 py-1.5 rounded-[10px] text-[13px] text-left cursor-default transition-all duration-200 hover-lift mb-1 ${
isSelected
? 'bg-accent text-white font-medium'
: 'text-text-primary hover:bg-item-hover'
? 'bg-accent text-white font-medium shadow-sm shadow-accent/20 scale-[0.98]'
: 'text-text-primary hover:bg-item-hover hover:text-text-primary'
}`}
onClick={() => onSelectFilter(filter)}
>
<Icon className={`w-4 h-4 mr-2 ${isSelected ? 'text-white' : 'text-text-secondary'}`} strokeWidth={isSelected ? 2.25 : 2} />
<Icon className={`w-4 h-4 mr-2.5 transition-colors ${isSelected ? 'text-white' : 'text-text-secondary group-hover:text-accent'}`} strokeWidth={isSelected ? 2.5 : 2} />
<span className="truncate">{label}</span>
{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 ${
isSelected ? 'bg-black/20 text-white' : 'bg-item-hover text-text-secondary'
<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 ${
isSelected ? 'bg-black/20 text-white' : 'bg-item-hover text-text-secondary group-hover:bg-item-hover'
}`}>
{getCount(filter)}
</span>
@@ -132,17 +132,17 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
type="button"
onContextMenu={e => handleQueueContextMenu(e, queue.id)}
onClick={() => onSelectFilter(filterId)}
className={`group flex w-full items-center px-2.5 py-1.5 rounded-lg text-[13px] text-left cursor-default transition-colors mb-0.5 ${
className={`group flex w-full items-center px-3 py-1.5 rounded-[10px] text-[13px] text-left cursor-default transition-all duration-200 hover-lift mb-1 ${
isSelected
? 'bg-accent text-white font-medium'
: 'text-text-primary hover:bg-item-hover'
? 'bg-accent text-white font-medium shadow-sm shadow-accent/20 scale-[0.98]'
: 'text-text-primary hover:bg-item-hover hover:text-text-primary'
}`}
>
<List className={`w-4 h-4 mr-2 shrink-0 ${isSelected ? 'text-white' : 'text-text-secondary'}`} strokeWidth={isSelected ? 2.25 : 2} />
<List className={`w-4 h-4 mr-2.5 shrink-0 transition-colors ${isSelected ? 'text-white' : 'text-text-secondary group-hover:text-accent'}`} strokeWidth={isSelected ? 2.5 : 2} />
<span className="truncate">{queue.name}</span>
{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 ${
isSelected ? 'bg-black/20 text-white' : 'bg-item-hover text-text-secondary'
<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 ${
isSelected ? 'bg-black/20 text-white' : 'bg-item-hover text-text-secondary group-hover:bg-item-hover'
}`}>
{getCount(filterId)}
</span>
@@ -157,11 +157,11 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
<button
type="button"
onClick={() => setActiveView(view)}
className={`flex w-full items-center px-2.5 py-1.5 rounded-lg text-[13px] text-left cursor-default transition-colors mb-0.5 ${
isSelected ? 'bg-accent text-white font-medium' : 'text-text-primary hover:bg-item-hover'
className={`group flex w-full items-center px-3 py-1.5 rounded-[10px] text-[13px] text-left cursor-default transition-all duration-200 hover-lift mb-1 ${
isSelected ? 'bg-accent text-white font-medium shadow-sm shadow-accent/20 scale-[0.98]' : 'text-text-primary hover:bg-item-hover hover:text-text-primary'
}`}
>
<Icon className={`w-4 h-4 mr-2 ${isSelected ? 'text-white' : 'text-text-secondary'}`} strokeWidth={isSelected ? 2.25 : 2} />
<Icon className={`w-4 h-4 mr-2.5 transition-colors ${isSelected ? 'text-white' : 'text-text-secondary group-hover:text-accent'}`} strokeWidth={isSelected ? 2.5 : 2} />
<span>{label}</span>
</button>
);
@@ -170,17 +170,17 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
return (
<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">
<div className="text-[11px] font-semibold text-text-muted px-2.5 mb-1.5">Library</div>
<div className="overflow-y-auto flex-1 px-3 pb-3">
<section className="mb-5">
<div className="text-[11px] font-bold tracking-wider text-text-muted/70 uppercase px-3 mb-2">Library</div>
<NavItem icon={Inbox} label="All" filter="all" />
<NavItem icon={Zap} label="Active" filter="active" />
<NavItem icon={CheckCircle2} label="Completed" filter="completed" />
<NavItem icon={CircleDashed} label="Unfinished" filter="unfinished" />
</section>
<section className="mb-4">
<div className="text-[11px] font-semibold text-text-muted px-2.5 mb-1.5">Folders</div>
<section className="mb-5">
<div className="text-[11px] font-bold tracking-wider text-text-muted/70 uppercase px-3 mb-2">Folders</div>
<NavItem icon={Music} label="Musics" filter="Musics" />
<NavItem icon={Film} label="Movies" filter="Movies" />
<NavItem icon={Archive} label="Compressed" filter="Compressed" />
@@ -190,8 +190,8 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
<NavItem icon={FileQuestion} label="Other" filter="Other" />
</section>
<section className="mb-4">
<div className="text-[11px] font-semibold text-text-muted px-2.5 mb-1.5">Queues</div>
<section className="mb-5">
<div className="text-[11px] font-bold tracking-wider text-text-muted/70 uppercase px-3 mb-2">Queues</div>
{queues.map(queue => (
<QueueItem key={queue.id} queue={queue} />
))}
@@ -224,8 +224,8 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
)}
</section>
<section>
<div className="text-[11px] font-semibold text-text-muted px-2.5 mb-1.5">Tools</div>
<section className="mt-auto pt-4 border-t border-border-color/30">
<div className="text-[11px] font-bold tracking-wider text-text-muted/70 uppercase px-3 mb-2">Tools</div>
<ToolItem icon={CalendarClock} label="Scheduler" view="scheduler" />
<ToolItem icon={Gauge} label="Speed Limiter" view="speedLimiter" />
</section>
+92 -61
View File
@@ -1,92 +1,90 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
@import "tailwindcss";
:root {
/* Default/fallback Light, matching Theme.modernLight. */
/* Default/fallback Light */
--main-bg: 0 0% 98%;
--sidebar-bg: 0 0% 94%;
--sidebar-glass: 0 0% 94% / 0.78;
--sidebar-bg: 0 0% 100% / 0.6;
--sidebar-glass: 0 0% 100% / 0.6;
--border-color: 0 0% 88%;
--item-hover: 0 0% 0% / 0.05;
--item-selected: 211 100% 50% / 0.15;
--accent-color: 211 100% 52%;
--text-primary: 0 0% 10%;
--text-secondary: 0 0% 30%;
--text-muted: 0 0% 50%;
--text-primary: 0 0% 15%;
--text-secondary: 0 0% 35%;
--text-muted: 0 0% 55%;
--bg-modal: 0 0% 100%;
--bg-input: 0 0% 100%;
--bg-input: 0 0% 95%;
--border-modal: 0 0% 85%;
}
.theme-light {
color-scheme: light;
--main-bg: 0 0% 98%;
--sidebar-bg: 0 0% 94%;
--sidebar-glass: 0 0% 94% / 0.78;
--border-color: 0 0% 88%;
--main-bg: 210 20% 98%;
--sidebar-bg: 0 0% 100% / 0.7;
--sidebar-glass: 0 0% 100% / 0.7;
--border-color: 210 10% 88%;
--item-hover: 0 0% 0% / 0.05;
--item-selected: 211 100% 50% / 0.15;
--accent-color: 211 100% 52%;
--text-primary: 0 0% 10%;
--text-secondary: 0 0% 30%;
--text-muted: 0 0% 50%;
--item-selected: 220 90% 56% / 0.15;
--accent-color: 220 90% 56%;
--text-primary: 220 20% 15%;
--text-secondary: 220 15% 40%;
--text-muted: 220 10% 55%;
--bg-modal: 0 0% 100%;
--bg-input: 0 0% 100%;
--bg-input: 0 0% 0% / 0.04;
--border-modal: 0 0% 85%;
}
.theme-dark {
color-scheme: dark;
/* Native dark approximation for SwiftUI's system appearance. */
--main-bg: 0 0% 16%;
--sidebar-bg: 0 0% 14%;
--sidebar-glass: 0 0% 14% / 0.78;
--border-color: 0 0% 24%;
--item-hover: 0 0% 100% / 0.08;
--item-selected: 211 100% 50% / 0.2;
--accent-color: 211 100% 52%;
--text-primary: 0 0% 98%;
--text-secondary: 0 0% 75%;
--text-muted: 0 0% 55%;
--bg-modal: 0 0% 18%;
--bg-input: 0 0% 0% / 0.3;
--border-modal: 0 0% 28%;
--main-bg: 0 0% 6%; /* Deep, pure dark background */
--sidebar-bg: 0 0% 10% / 0.7;
--sidebar-glass: 0 0% 10% / 0.7;
--border-color: 0 0% 100% / 0.08; /* Subtle white opacity borders */
--item-hover: 0 0% 100% / 0.05;
--item-selected: 217 91% 60% / 0.15;
--accent-color: 217 91% 60%;
--text-primary: 0 0% 93%;
--text-secondary: 0 0% 60%;
--text-muted: 0 0% 40%;
--bg-modal: 0 0% 12%;
--bg-input: 0 0% 0% / 0.4;
--border-modal: 0 0% 100% / 0.1;
}
.theme-dracula {
color-scheme: dark;
/* Exact values from Theme.dracula. */
--main-bg: 240 14% 19%;
--sidebar-bg: 233 13% 31%;
--sidebar-glass: 233 13% 31% / 0.78;
--border-color: 232 13% 38%;
--item-hover: 0 0% 100% / 0.08;
--item-selected: 340 100% 74% / 0.2;
--accent-color: 340 100% 74%;
--text-primary: 60 25% 96%;
--text-secondary: 222 21% 48%;
--text-muted: 222 17% 62%;
--bg-modal: 240 14% 21%;
--bg-input: 240 14% 15%;
--border-modal: 233 13% 39%;
--main-bg: 231 15% 12%;
--sidebar-bg: 231 15% 16% / 0.7;
--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;
}
.theme-nord {
color-scheme: dark;
/* Exact values from Theme.nord. */
--main-bg: 220 16% 22%;
--sidebar-bg: 221 16% 28%;
--sidebar-glass: 221 16% 28% / 0.78;
--border-color: 220 15% 35%;
--item-hover: 0 0% 100% / 0.08;
--main-bg: 220 16% 16%;
--sidebar-bg: 220 16% 20% / 0.7;
--sidebar-glass: 220 16% 20% / 0.7;
--border-color: 0 0% 100% / 0.08;
--item-hover: 0 0% 100% / 0.05;
--item-selected: 193 43% 67% / 0.2;
--accent-color: 194 44% 67%;
--text-primary: 220 25% 88%;
--text-secondary: 212 18% 64%;
--text-muted: 212 14% 72%;
--bg-modal: 220 16% 25%;
--bg-input: 220 16% 19%;
--border-modal: 220 15% 36%;
--accent-color: 193 43% 67%;
--text-primary: 218 27% 92%;
--text-secondary: 213 20% 65%;
--text-muted: 213 15% 50%;
--bg-modal: 220 16% 20%;
--bg-input: 0 0% 0% / 0.4;
--border-modal: 0 0% 100% / 0.1;
}
@theme {
@@ -105,7 +103,7 @@
--color-bg-input: hsl(var(--bg-input));
--color-border-modal: hsl(var(--border-modal));
--font-sans: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
@layer base {
@@ -118,6 +116,7 @@
-webkit-user-select: none;
cursor: default;
-webkit-font-smoothing: antialiased;
-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);
}
@@ -157,10 +156,24 @@
}
}
/* Glassmorphism Classes */
.glass-panel {
background: var(--color-sidebar-glass);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(28px);
-webkit-backdrop-filter: blur(28px);
}
.glass-card {
background: var(--color-sidebar-glass);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--color-border-color);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
}
/* Base App Background Gradient - Removed for Modern Minimalist Look */
.app-bg-gradient {
background: transparent;
}
.settings-tab-label {
@@ -173,3 +186,21 @@
letter-spacing: -0.01em;
}
}
/* Micro Animations */
@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-3px); }
100% { transform: translateY(0px); }
}
.animate-float {
animation: float 4s ease-in-out infinite;
}
.hover-lift {
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.hover-lift:hover {
transform: translateY(-1px) scale(1.01);
}
+4 -1
View File
@@ -394,7 +394,10 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
speedLimit,
username: item.username || (login ? login.username : null),
password: item.password || keychainPassword,
headers: item.headers || null
headers: item.headers || null,
proxy: getProxyArgs(settings),
userAgent: settings.customUserAgent || null,
maxTries: settings.maxAutomaticRetries
});
} else {
const speedLimit = effectiveSpeedLimit(
+11 -2
View File
@@ -1,5 +1,6 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { invoke } from '@tauri-apps/api/core';
export interface SiteLogin {
id: string;
@@ -56,6 +57,7 @@ export interface SettingsState {
downloadDirectories: Record<string, string>;
siteLogins: SiteLogin[];
extensionPairingToken: string;
autoCheckUpdates: boolean;
setTheme: (theme: 'dark' | 'light' | 'system' | 'dracula' | 'nord') => void;
setDefaultDownloadPath: (path: string) => void;
@@ -90,6 +92,7 @@ export interface SettingsState {
addSiteLogin: (login: SiteLogin) => void;
removeSiteLogin: (id: string) => void;
regeneratePairingToken: () => void;
setAutoCheckUpdates: (autoCheckUpdates: boolean) => void;
}
const defaultDirectories = {
@@ -192,10 +195,14 @@ export const useSettingsStore = create<SettingsState>()(
downloadDirectories: { ...defaultDirectories },
siteLogins: [],
extensionPairingToken: generateSecureToken(),
autoCheckUpdates: true,
setTheme: (theme) => set({ theme }),
setDefaultDownloadPath: (path) => set({ defaultDownloadPath: path }),
setMaxConcurrentDownloads: (max) => set({ maxConcurrentDownloads: max }),
setMaxConcurrentDownloads: (max) => {
set({ maxConcurrentDownloads: max });
invoke('set_concurrent_limit', { limit: max }).catch(console.error);
},
setGlobalSpeedLimit: (limit) => set({ globalSpeedLimit: limit }),
setActiveView: (view) => set({ activeView: view }),
setActiveSettingsTab: (activeSettingsTab) => set({ activeSettingsTab }),
@@ -232,6 +239,7 @@ export const useSettingsStore = create<SettingsState>()(
siteLogins: state.siteLogins.filter((login) => login.id !== id)
})),
regeneratePairingToken: () => set({ extensionPairingToken: generateSecureToken() }),
setAutoCheckUpdates: (autoCheckUpdates) => set({ autoCheckUpdates }),
}),
{
name: 'firelink-settings',
@@ -264,7 +272,8 @@ export const useSettingsStore = create<SettingsState>()(
mediaCookieSource: state.mediaCookieSource,
downloadDirectories: state.downloadDirectories,
siteLogins: state.siteLogins,
extensionPairingToken: state.extensionPairingToken
extensionPairingToken: state.extensionPairingToken,
autoCheckUpdates: state.autoCheckUpdates
}),
merge: (persistedState: any, currentState) => ({
...currentState,