feat(desktop): modernize dark theme and update flow

This commit is contained in:
NimBold
2026-06-13 18:08:42 +03:30
parent ba37d78cba
commit 19e366089e
15 changed files with 489 additions and 203 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "tauri-app",
"version": "0.1.0",
"version": "0.7.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tauri-app",
"version": "0.1.0",
"version": "0.7.3",
"dependencies": {
"@tailwindcss/vite": "^4.3.0",
"@tauri-apps/api": "^2",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "tauri-app",
"private": true,
"version": "0.1.0",
"version": "0.7.3",
"type": "module",
"scripts": {
"dev": "vite",
+1 -1
View File
@@ -3928,7 +3928,7 @@ dependencies = [
[[package]]
name = "tauri-app"
version = "0.1.0"
version = "0.7.3"
dependencies = [
"hmac",
"keyring",
+3 -3
View File
@@ -1,8 +1,8 @@
[package]
name = "tauri-app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
version = "0.7.3"
description = "A native download manager for fast, organized, segmented transfers"
authors = ["NimBold"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Firelink",
"version": "0.1.0",
"version": "0.7.3",
"identifier": "com.nimbold.firelink",
"build": {
"beforeDevCommand": "npm run dev",
+8 -8
View File
@@ -236,21 +236,21 @@ function App() {
}, []);
return (
<div className="flex h-screen w-screen bg-main-bg app-bg-gradient text-text-primary overflow-hidden">
<div className="app-shell flex h-screen w-screen text-text-primary overflow-hidden">
{/* Left Side Panel - Curved Second Layer on Top */}
<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'
className={`app-sidebar flex flex-col overflow-hidden relative z-20 shrink-0 transition-all duration-300 ease-in-out ${
isSidebarVisible ? 'w-[244px] opacity-100' : 'w-0 opacity-0'
}`}
>
<div className="w-[240px] h-full flex flex-col shrink-0">
<div className="w-[244px] 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">
<div className="app-workspace flex-1 flex flex-col h-full relative z-0">
<div className="flex-1 flex flex-col overflow-hidden relative">
{activeView === 'downloads' && <DownloadTable filter={filter} />}
{activeView === 'settings' && <SettingsView />}
@@ -259,12 +259,12 @@ function App() {
</div>
{/* Status Bar */}
<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">
<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="w-1.5 h-1.5 rounded-full bg-accent/80 animate-pulse"></span>
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
Ready
</span>
<div className="flex gap-4">
<div className="flex gap-3 tabular-nums">
<span className="flex items-center gap-1.5">
<span className="text-text-primary">{activeDownloadCount}</span> active
</span>
@@ -625,8 +625,8 @@ export const AddDownloadsModal = () => {
onCancel={() => setShowingDuplicates(false)}
/>
)}
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md">
<div className="w-[900px] h-[650px] bg-bg-modal border border-border-modal rounded-xl shadow-2xl flex flex-col overflow-hidden text-sm">
<div className="app-modal-backdrop fixed inset-0 z-50 flex items-center justify-center">
<div className="app-modal w-[900px] h-[650px] flex flex-col overflow-hidden text-sm">
{/* Main Content Split */}
<div className="flex flex-1 overflow-hidden">
@@ -643,7 +643,7 @@ export const AddDownloadsModal = () => {
</div>
</div>
<textarea
className="w-full h-32 bg-bg-input/80 border border-border-modal rounded-lg p-3 text-[13px] text-text-primary focus:outline-none focus:border-blue-500 resize-none font-mono shadow-inner transition-colors"
className="w-full h-32 bg-bg-input/80 border border-border-modal rounded-lg p-3 text-[13px] text-text-primary focus:outline-none focus:border-accent resize-none font-mono shadow-inner transition-colors"
placeholder="Paste HTTP, HTTPS, FTP, or SFTP URLs here..."
value={urls}
onChange={(e) => setUrls(e.target.value)}
@@ -719,7 +719,7 @@ export const AddDownloadsModal = () => {
{/* Media Format (Dynamic) */}
{selectedItemIndex !== null && parsedItems[selectedItemIndex]?.isMedia && (
<section className="bg-gradient-to-br from-purple-500/5 to-blue-500/5 border border-purple-500/20 rounded-xl p-4 shadow-sm relative overflow-hidden">
<section className="app-card relative overflow-hidden p-4">
<div className="absolute top-0 right-0 p-2 opacity-10">
<Video size={48} />
</div>
@@ -805,7 +805,7 @@ export const AddDownloadsModal = () => {
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<label className="text-xs text-text-secondary font-medium">Target Queue</label>
<select value={selectedQueueId} onChange={e=>setSelectedQueueId(e.target.value)} className="w-32 bg-bg-input border border-border-modal rounded-md px-2 py-1 text-xs text-text-primary focus:border-blue-500 focus:outline-none">
<select value={selectedQueueId} onChange={e=>setSelectedQueueId(e.target.value)} className="w-32 bg-bg-input border border-border-modal rounded-lg px-2 py-1 text-xs text-text-primary focus:border-accent focus:outline-none">
{queues.map(q => (
<option key={q.id} value={q.id}>{q.name}</option>
))}
@@ -827,7 +827,7 @@ export const AddDownloadsModal = () => {
</label>
{speedLimitEnabled && (
<div className="flex items-center gap-1.5">
<input type="number" value={speedLimit} onChange={e=>setSpeedLimit(e.target.value)} className="w-16 bg-bg-input border border-border-modal rounded px-2 py-1 text-xs font-mono text-text-primary focus:border-blue-500 focus:outline-none" />
<input type="number" value={speedLimit} onChange={e=>setSpeedLimit(e.target.value)} className="w-16 bg-bg-input border border-border-modal rounded-lg px-2 py-1 text-xs font-mono text-text-primary focus:border-accent focus:outline-none" />
<span className="text-[10px] text-text-muted">KiB/s</span>
</div>
)}
@@ -847,8 +847,8 @@ export const AddDownloadsModal = () => {
{useAuth && (
<div className="space-y-2.5 pl-5 border-l-2 border-border-modal/50">
<input type="text" value={username} onChange={e=>setUsername(e.target.value)} placeholder="Username" className="w-full bg-bg-input border border-border-modal rounded-md px-3 py-1.5 text-xs text-text-primary focus:border-blue-500 focus:outline-none" />
<input type="password" value={password} onChange={e=>setPassword(e.target.value)} placeholder="Password" className="w-full bg-bg-input border border-border-modal rounded-md px-3 py-1.5 text-xs text-text-primary focus:border-blue-500 focus:outline-none" />
<input type="text" value={username} onChange={e=>setUsername(e.target.value)} placeholder="Username" className="w-full bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs text-text-primary focus:border-accent focus:outline-none" />
<input type="password" value={password} onChange={e=>setPassword(e.target.value)} placeholder="Password" className="w-full bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs text-text-primary focus:border-accent focus:outline-none" />
</div>
)}
</section>
@@ -872,24 +872,24 @@ export const AddDownloadsModal = () => {
{checksumEnabled && (
<div className="flex gap-2">
<select value={checksumAlgo} onChange={e=>setChecksumAlgo(e.target.value)} className="w-24 bg-bg-input border border-border-modal rounded-md px-2 text-xs text-text-primary focus:border-blue-500 focus:outline-none">
<select value={checksumAlgo} onChange={e=>setChecksumAlgo(e.target.value)} className="w-24 bg-bg-input border border-border-modal rounded-lg px-2 text-xs text-text-primary focus:border-accent focus:outline-none">
<option>MD5</option><option>SHA-1</option><option>SHA-256</option>
</select>
<input type="text" value={checksumValue} onChange={e=>setChecksumValue(e.target.value)} placeholder="Expected digest" className="flex-1 bg-bg-input border border-border-modal rounded-md px-3 py-1.5 text-xs font-mono text-text-primary focus:border-blue-500 focus:outline-none" />
<input type="text" value={checksumValue} onChange={e=>setChecksumValue(e.target.value)} placeholder="Expected digest" className="flex-1 bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs font-mono text-text-primary focus:border-accent focus:outline-none" />
</div>
)}
<div>
<label className="block text-[10px] uppercase font-bold tracking-wider text-text-muted mb-1">Headers</label>
<textarea value={headers} onChange={e=>setHeaders(e.target.value)} className="w-full h-12 bg-bg-input border border-border-modal rounded-md px-3 py-1.5 text-xs font-mono text-text-primary focus:border-blue-500 focus:outline-none resize-none" />
<textarea value={headers} onChange={e=>setHeaders(e.target.value)} className="w-full h-12 bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs font-mono text-text-primary focus:border-accent focus:outline-none resize-none" />
</div>
<div>
<label className="block text-[10px] uppercase font-bold tracking-wider text-text-muted mb-1">Cookies</label>
<input type="text" value={cookies} onChange={e=>setCookies(e.target.value)} placeholder="name=value; other=value" className="w-full bg-bg-input border border-border-modal rounded-md px-3 py-1.5 text-xs font-mono text-text-primary focus:border-blue-500 focus:outline-none" />
<input type="text" value={cookies} onChange={e=>setCookies(e.target.value)} placeholder="name=value; other=value" className="w-full bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs font-mono text-text-primary focus:border-accent focus:outline-none" />
</div>
<div>
<label className="block text-[10px] uppercase font-bold tracking-wider text-text-muted mb-1">Mirrors</label>
<textarea value={mirrors} onChange={e=>setMirrors(e.target.value)} className="w-full h-12 bg-bg-input border border-border-modal rounded-md px-3 py-1.5 text-xs font-mono text-text-primary focus:border-blue-500 focus:outline-none resize-none" />
<textarea value={mirrors} onChange={e=>setMirrors(e.target.value)} className="w-full h-12 bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs font-mono text-text-primary focus:border-accent focus:outline-none resize-none" />
</div>
</div>
)}
@@ -905,20 +905,20 @@ export const AddDownloadsModal = () => {
{parsedItems.length === 0 ? "Paste one or more links." : `Ready to add ${parsedItems.length} download(s).`}
</div>
<div className="flex gap-2.5">
<button onClick={() => toggleAddModal(false)} className="px-4 py-1.5 rounded-lg text-xs font-medium text-text-secondary hover:text-text-primary hover:bg-item-hover transition-colors">
<button onClick={() => toggleAddModal(false)} className="app-button border-transparent bg-transparent px-4 text-xs text-text-secondary">
Cancel
</button>
<button
onClick={() => handleStart(false)}
disabled={parsedItems.length === 0}
className="px-4 py-1.5 rounded-lg text-xs font-medium bg-item-hover text-text-primary border border-border-modal hover:bg-border-modal/40 transition-colors disabled:opacity-50"
className="app-button px-4 text-xs disabled:opacity-50"
>
Add to Queue
</button>
<button
onClick={() => handleStart(true)}
disabled={parsedItems.length === 0}
className="px-5 py-1.5 rounded-lg text-xs font-medium bg-blue-600 hover:bg-blue-500 text-white shadow-md shadow-blue-500/20 transition-all active:scale-95 disabled:opacity-50 flex items-center gap-1.5"
className="app-button app-button-primary px-5 text-xs disabled:opacity-50"
>
<Play size={12} fill="currentColor" /> Start Downloads
</button>
+57 -40
View File
@@ -13,7 +13,7 @@ interface DownloadTableProps {
export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
const { downloads, toggleAddModal, updateDownload, removeDownload, clearFinished, redownload } = useDownloadStore();
const { isSidebarVisible, toggleSidebar } = useSettingsStore();
const { isSidebarVisible, toggleSidebar, listRowDensity } = useSettingsStore();
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null);
@@ -87,6 +87,11 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
};
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) => {
switch(category) {
@@ -102,64 +107,69 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
}
return (
<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">
<div className="flex-1 flex flex-col bg-transparent h-full relative p-3 pb-0">
<div className="app-surface shrink-0 rounded-xl mb-3 z-10">
<WindowDragRegion className={!isSidebarVisible ? 'pl-20' : ''} />
{/* Download Toolbar */}
<div className={`flex px-5 pb-3 pt-1 items-center ${!isSidebarVisible ? 'pl-20' : ''}`}>
<div className={`flex px-3 pb-2.5 pt-0.5 items-center ${!isSidebarVisible ? 'pl-20' : ''}`}>
<button
onClick={toggleSidebar}
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"
className="app-icon-button mr-2"
title="Toggle Sidebar"
>
<PanelLeft size={18} strokeWidth={2.5} />
<PanelLeft size={17} strokeWidth={2} />
</button>
<h2 className="text-[15px] font-bold mr-auto text-text-primary tracking-tight cursor-default">{getFilterTitle()}</h2>
<div className="mr-auto">
<h2 className="text-[14px] font-semibold text-text-primary tracking-tight cursor-default">{getFilterTitle()}</h2>
<p className="mt-0.5 text-[10px] text-text-muted tabular-nums">
{filteredDownloads.length} {filteredDownloads.length === 1 ? 'item' : 'items'}
</p>
</div>
<div className="flex gap-2 items-center bg-item-hover/30 p-1 rounded-xl border border-border-color/30">
<div className="flex items-center gap-0.5">
<button
onClick={() => toggleAddModal(true)}
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"
className="app-icon-button text-accent"
title="Add Download"
>
<Plus size={16} strokeWidth={2.5} />
<Plus size={17} strokeWidth={2.25} />
</button>
<div className="w-[1px] h-4 bg-border-color/50"></div>
<div className="mx-1 h-4 w-px bg-border-color"></div>
<button
onClick={() => {
filteredDownloads.filter(d => d.status === 'paused').forEach(d => handleResume(d));
}}
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"
className="app-icon-button"
title="Resume All"
>
<Play size={16} fill="currentColor" className="opacity-90" />
<Play size={15} fill="currentColor" className="opacity-80" />
</button>
<button
onClick={() => {
filteredDownloads.filter(d => d.status === 'downloading').forEach(d => handlePause(d.id));
}}
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"
className="app-icon-button"
title="Pause All"
>
<Pause size={16} fill="currentColor" className="opacity-90" />
<Pause size={15} fill="currentColor" className="opacity-80" />
</button>
<div className="w-[1px] h-4 bg-border-color/50"></div>
<div className="mx-1 h-4 w-px bg-border-color"></div>
<button
onClick={clearFinished}
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"
className="app-icon-button hover:text-red-400"
title="Clear Finished"
>
<Trash2 size={16} strokeWidth={2} />
<Trash2 size={15} strokeWidth={1.9} />
</button>
</div>
</div>
</div>
{/* List */}
<div className="flex-1 overflow-auto bg-transparent px-1 pb-4 relative">
<div className="flex-1 overflow-auto bg-transparent pb-3 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 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">
<div className="flex-1 min-w-[200px]">FILE</div>
<div className="w-32">SIZE</div>
<div className="w-32">STATUS</div>
@@ -169,19 +179,25 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
</div>
<div className="flex flex-col gap-2">
{filteredDownloads.length === 0 ? (
<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 className="app-card mx-1 mt-2 py-14 text-center">
<div className="flex flex-col items-center justify-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-2xl border border-border-color bg-bg-input text-text-muted">
<Box size={23} strokeWidth={1.6} />
</div>
<span className="text-[13px] font-medium tracking-wide">No downloads in this view</span>
<div>
<p className="text-[13px] font-semibold text-text-primary">No downloads here</p>
<p className="mt-1 text-[11px] text-text-muted">Add a link to start a new transfer.</p>
</div>
<button onClick={() => toggleAddModal(true)} className="app-button app-button-primary mt-1 px-3 text-[11px]">
<Plus size={14} /> Add Download
</button>
</div>
</div>
) : (
filteredDownloads.map(d => (
<div
key={d.id}
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"
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`}
onContextMenu={(e) => {
e.preventDefault();
setContextMenu({
@@ -193,7 +209,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
>
<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.5 bg-bg-modal shadow-sm rounded-xl border border-border-color/40 text-text-muted group-hover:scale-105 transition-transform">
<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)}
</div>
<span className="font-medium truncate max-w-[280px]">{d.fileName}</span>
@@ -233,12 +249,12 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
</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">
{d.status === 'downloading' && (
<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">
<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">
<Pause size={14} fill="currentColor" />
</button>
)}
{d.status === 'paused' && (
<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">
<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">
<Play size={14} fill="currentColor" />
</button>
)}
@@ -247,7 +263,8 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
e.stopPropagation();
setContextMenu({ x: e.clientX, y: e.clientY, id: d.id });
}}
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"
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>
@@ -264,7 +281,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
{/* Floating Context Menu */}
{contextMenu && contextItem && (
<div
className="fixed z-50 bg-bg-modal/95 backdrop-blur-xl border border-border-modal rounded-xl shadow-2xl py-1.5 min-w-[180px] text-[13px] font-medium text-text-primary overflow-hidden"
className="app-modal fixed z-50 min-w-[180px] overflow-hidden py-1.5 text-[12px] font-medium text-text-primary"
style={{
top: Math.min(contextMenu.y, window.innerHeight - 300),
left: Math.min(contextMenu.x, window.innerWidth - 200)
@@ -282,7 +299,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
console.error("Failed to open file:", e);
}
}}
className="w-full text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors"
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
>
Open File
</button>
@@ -298,7 +315,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
console.error("Failed to show in folder:", e);
}
}}
className="w-full text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors"
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
>
Show in Finder
</button>
@@ -311,7 +328,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
setContextMenu(null);
handlePause(contextItem.id);
}}
className="w-full text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors"
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
>
Pause
</button>
@@ -323,7 +340,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
setContextMenu(null);
handleResume(contextItem);
}}
className="w-full text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors"
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
>
Resume
</button>
@@ -335,7 +352,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
setContextMenu(null);
redownload(contextItem.id);
}}
className="w-full text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors"
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
>
Redownload
</button>
@@ -348,7 +365,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
setContextMenu(null);
navigator.clipboard.writeText(contextItem.url);
}}
className="w-full text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors"
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
>
Copy Address
</button>
@@ -360,7 +377,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
const fullPath = await resolvePath(contextItem.destination || '~/Downloads', contextItem.fileName);
navigator.clipboard.writeText(fullPath);
}}
className="w-full text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors"
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
>
Copy File Path
</button>
@@ -373,7 +390,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
setContextMenu(null);
handleDelete(contextItem.id);
}}
className="w-full text-left px-4 py-1.5 hover:bg-red-500 hover:text-white text-red-500 transition-colors"
className="w-full text-left px-3 py-2 text-red-400 hover:bg-red-500/10 transition-colors"
>
Remove from List
</button>
@@ -385,7 +402,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
setContextMenu(null);
useDownloadStore.getState().setSelectedPropertiesDownloadId(contextItem.id);
}}
className="w-full text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors"
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
>
Properties
</button>
@@ -23,8 +23,8 @@ export const DuplicateResolutionModal = ({ conflicts: initialConflicts, onConfir
};
return (
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 backdrop-blur-md">
<div className="w-[500px] bg-bg-modal border border-border-modal rounded-xl shadow-2xl flex flex-col overflow-hidden text-sm">
<div className="app-modal-backdrop fixed inset-0 z-[60] flex items-center justify-center">
<div className="app-modal w-[500px] flex flex-col overflow-hidden text-sm">
<div className="p-4 border-b border-border-modal flex flex-col gap-2">
<h2 className="text-lg font-semibold text-text-primary">Duplicate Downloads Detected</h2>
<p className="text-xs text-text-muted">Some of the downloads you are adding already exist in the queue or on disk. Please choose how to resolve these conflicts.</p>
@@ -40,7 +40,7 @@ export const DuplicateResolutionModal = ({ conflicts: initialConflicts, onConfir
<select
value={conflict.resolution}
onChange={(e) => updateResolution(conflict.id, e.target.value as any)}
className="w-24 shrink-0 bg-bg-input border border-border-modal rounded text-xs px-2 py-1 text-text-primary focus:border-blue-500 focus:outline-none"
className="app-control w-24 shrink-0 px-2 py-1 text-xs"
>
<option value="rename">Rename</option>
<option value="replace">Replace</option>
@@ -51,12 +51,12 @@ export const DuplicateResolutionModal = ({ conflicts: initialConflicts, onConfir
</div>
<div className="p-4 border-t border-border-modal flex items-center justify-between bg-sidebar-bg/50">
<button onClick={onCancel} className="px-4 py-1.5 rounded-lg text-xs font-medium text-text-secondary hover:text-text-primary hover:bg-item-hover transition-colors">
<button onClick={onCancel} className="app-button border-transparent bg-transparent px-4 text-xs text-text-secondary">
Cancel
</button>
<button
onClick={() => onConfirm(conflicts.map(c => ({ id: c.id, resolution: c.resolution })))}
className="px-5 py-1.5 rounded-lg text-xs font-medium bg-blue-600 hover:bg-blue-500 text-white shadow-md shadow-blue-500/20 transition-all active:scale-95"
className="app-button app-button-primary px-5 text-xs"
>
Continue
</button>
+16 -16
View File
@@ -148,8 +148,8 @@ export const PropertiesModal = () => {
else if (item.status === 'failed') { statusColor = 'text-red-500'; StatusIcon = AlertCircle; }
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="w-[720px] h-[580px] bg-bg-modal border border-border-modal rounded-xl shadow-2xl flex flex-col overflow-hidden text-sm">
<div className="app-modal-backdrop fixed inset-0 z-50 flex items-center justify-center">
<div className="app-modal w-[720px] h-[580px] flex flex-col overflow-hidden text-sm">
{/* Header Summary */}
<div className="p-4 px-5 bg-sidebar-bg/50">
@@ -202,14 +202,14 @@ export const PropertiesModal = () => {
<h3 className="text-sm font-semibold text-text-primary mb-4 pb-1 border-b border-border-modal/50">Download</h3>
<div className="grid grid-cols-[100px_1fr] gap-y-3.5 gap-x-4 items-center">
<label className="text-xs text-text-muted text-right">URL</label>
<input type="text" value={url} onChange={e => setUrl(e.target.value)} disabled={isLocked} className="bg-bg-input border border-border-modal rounded px-2.5 py-1.5 text-xs text-text-primary font-mono focus:outline-none focus:border-blue-500 disabled:opacity-50" />
<input type="text" value={url} onChange={e => setUrl(e.target.value)} disabled={isLocked} className="bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary font-mono focus:outline-none focus:border-accent disabled:opacity-50" />
<label className="text-xs text-text-muted text-right">File name</label>
<input type="text" value={fileName} onChange={e => setFileName(e.target.value)} disabled={isLocked} className="bg-bg-input border border-border-modal rounded px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-blue-500 disabled:opacity-50" />
<input type="text" value={fileName} onChange={e => setFileName(e.target.value)} disabled={isLocked} className="bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-accent disabled:opacity-50" />
<label className="text-xs text-text-muted text-right">Save location</label>
<div className="flex gap-2">
<input type="text" value={saveLocation} readOnly disabled={isLocked} className="flex-1 bg-bg-input border border-border-modal rounded px-2.5 py-1.5 text-xs text-text-primary font-mono focus:outline-none focus:border-blue-500 disabled:opacity-50" />
<input type="text" value={saveLocation} readOnly disabled={isLocked} className="flex-1 bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary font-mono focus:outline-none focus:border-accent disabled:opacity-50" />
<button onClick={handleBrowse} disabled={isLocked} className="bg-item-hover hover:bg-item-hover/80 text-text-primary border border-border-modal px-3 py-1.5 rounded text-xs transition-colors disabled:opacity-40 flex items-center gap-1.5">
<FolderPlus size={14} /> Select
</button>
@@ -217,7 +217,7 @@ export const PropertiesModal = () => {
<label className="text-xs text-text-muted text-right">Connections</label>
<div className="flex items-center gap-2">
<input type="number" value={connections} min={1} max={16} onChange={e=>setConnections(Number(e.target.value))} disabled={isTransferLocked} className="w-16 bg-bg-input border border-border-modal rounded px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-blue-500 disabled:opacity-50" />
<input type="number" value={connections} min={1} max={16} onChange={e=>setConnections(Number(e.target.value))} disabled={isTransferLocked} className="w-16 bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-accent disabled:opacity-50" />
<span className="text-xs text-text-muted">per file</span>
</div>
@@ -229,7 +229,7 @@ export const PropertiesModal = () => {
</label>
{speedLimitEnabled && (
<div className="flex items-center gap-2">
<input type="number" value={speedLimitValue} min={1} step={128} onChange={e=>setSpeedLimitValue(e.target.value)} className="w-20 bg-bg-input border border-border-modal rounded px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-blue-500" />
<input type="number" value={speedLimitValue} min={1} step={128} onChange={e=>setSpeedLimitValue(e.target.value)} className="w-20 bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-accent" />
<span className="text-xs text-text-muted">KiB/s</span>
</div>
)}
@@ -265,10 +265,10 @@ export const PropertiesModal = () => {
{loginMode === 'custom' && (
<>
<label className="text-xs text-text-muted text-right">Username</label>
<input type="text" value={username} onChange={e=>setUsername(e.target.value)} disabled={isTransferLocked} placeholder="Username" className="max-w-[250px] bg-bg-input border border-border-modal rounded px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-blue-500 disabled:opacity-50" />
<input type="text" value={username} onChange={e=>setUsername(e.target.value)} disabled={isTransferLocked} placeholder="Username" className="max-w-[250px] bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-accent disabled:opacity-50" />
<label className="text-xs text-text-muted text-right">Password</label>
<input type="password" value={password} onChange={e=>setPassword(e.target.value)} disabled={isTransferLocked} placeholder="Password" className="max-w-[250px] bg-bg-input border border-border-modal rounded px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-blue-500 disabled:opacity-50" />
<input type="password" value={password} onChange={e=>setPassword(e.target.value)} disabled={isTransferLocked} placeholder="Password" className="max-w-[250px] bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-accent disabled:opacity-50" />
</>
)}
</div>
@@ -295,7 +295,7 @@ export const PropertiesModal = () => {
{checksumEnabled && (
<>
<label className="text-xs text-text-muted text-right">Algorithm</label>
<select value={checksumAlgorithm} onChange={e=>setChecksumAlgorithm(e.target.value)} disabled={isTransferLocked} className="max-w-[150px] bg-bg-input border border-border-modal rounded px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-blue-500 disabled:opacity-50">
<select value={checksumAlgorithm} onChange={e=>setChecksumAlgorithm(e.target.value)} disabled={isTransferLocked} className="max-w-[150px] bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-accent disabled:opacity-50">
<option value="MD5">MD5</option>
<option value="SHA-1">SHA-1</option>
<option value="SHA-256">SHA-256</option>
@@ -303,21 +303,21 @@ export const PropertiesModal = () => {
</select>
<label className="text-xs text-text-muted text-right">Digest</label>
<input type="text" value={checksumValue} onChange={e=>setChecksumValue(e.target.value)} disabled={isTransferLocked} placeholder="Expected digest" className="bg-bg-input border border-border-modal rounded px-2.5 py-1.5 text-xs text-text-primary font-mono focus:outline-none focus:border-blue-500 disabled:opacity-50" />
<input type="text" value={checksumValue} onChange={e=>setChecksumValue(e.target.value)} disabled={isTransferLocked} placeholder="Expected digest" className="bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary font-mono focus:outline-none focus:border-accent disabled:opacity-50" />
</>
)}
<label className="text-xs text-text-muted text-right">Cookies</label>
<input type="text" value={cookies} onChange={e=>setCookies(e.target.value)} disabled={isTransferLocked} placeholder="Cookies" className="bg-bg-input border border-border-modal rounded px-2.5 py-1.5 text-xs text-text-primary font-mono focus:outline-none focus:border-blue-500 disabled:opacity-50" />
<input type="text" value={cookies} onChange={e=>setCookies(e.target.value)} disabled={isTransferLocked} placeholder="Cookies" className="bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary font-mono focus:outline-none focus:border-accent disabled:opacity-50" />
<div className="col-span-2 mt-2">
<label className="block text-xs text-text-muted mb-1.5">Headers</label>
<textarea value={headers} onChange={e=>setHeaders(e.target.value)} disabled={isTransferLocked} className="w-full h-16 bg-bg-input border border-border-modal rounded px-2.5 py-1.5 text-xs text-text-primary font-mono focus:outline-none focus:border-blue-500 disabled:opacity-50 resize-none"></textarea>
<textarea value={headers} onChange={e=>setHeaders(e.target.value)} disabled={isTransferLocked} className="w-full h-16 bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary font-mono focus:outline-none focus:border-accent disabled:opacity-50 resize-none"></textarea>
</div>
<div className="col-span-2">
<label className="block text-xs text-text-muted mb-1.5">Mirrors</label>
<textarea value={mirrors} onChange={e=>setMirrors(e.target.value)} disabled={isTransferLocked} className="w-full h-16 bg-bg-input border border-border-modal rounded px-2.5 py-1.5 text-xs text-text-primary font-mono focus:outline-none focus:border-blue-500 disabled:opacity-50 resize-none"></textarea>
<textarea value={mirrors} onChange={e=>setMirrors(e.target.value)} disabled={isTransferLocked} className="w-full h-16 bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary font-mono focus:outline-none focus:border-accent disabled:opacity-50 resize-none"></textarea>
</div>
</div>
)}
@@ -335,13 +335,13 @@ export const PropertiesModal = () => {
<div className="flex gap-2">
<button
onClick={() => setSelectedPropertiesDownloadId(null)}
className="px-4 py-1.5 rounded border border-border-modal text-xs font-medium text-text-secondary hover:text-text-primary hover:bg-item-hover transition-colors"
className="app-button border-transparent bg-transparent px-4 text-xs text-text-secondary"
>
Cancel
</button>
<button
onClick={handleSave}
className="px-4 py-1.5 rounded text-xs font-medium bg-blue-600 hover:bg-blue-500 text-white shadow-sm transition-all active:scale-95 flex items-center gap-1.5"
className="app-button app-button-primary px-4 text-xs"
>
<CheckCircle size={14} />
Save
+15 -15
View File
@@ -126,8 +126,8 @@ export default function SchedulerView() {
<div className="flex-1 flex h-full flex-col overflow-hidden bg-main-bg">
<WindowDragRegion />
<div className="flex items-center gap-4 border-b border-border-color px-6 pb-5">
<label className="flex items-center gap-3 text-xl font-bold text-text-primary">
<div className="flex items-center gap-3 border-b border-border-color px-6 pb-4">
<label className="flex items-center gap-3 text-[17px] font-semibold tracking-tight text-text-primary">
<input
type="checkbox"
checked={draft.enabled}
@@ -142,35 +142,35 @@ export default function SchedulerView() {
{schedulerRunning ? 'Running' : nextRun}
</span>
<div className="ml-auto flex gap-2">
<button onClick={runNow} className="flex items-center gap-2 rounded-md border border-border-modal bg-bg-input px-3 py-2 text-[12px] font-medium text-text-primary hover:bg-item-hover">
<button onClick={runNow} className="app-button px-3 text-[11px]">
<Play size={14} /> Run Now
</button>
<button onClick={pauseNow} className="flex items-center gap-2 rounded-md border border-border-modal bg-bg-input px-3 py-2 text-[12px] font-medium text-text-primary hover:bg-item-hover">
<button onClick={pauseNow} className="app-button px-3 text-[11px]">
<Pause size={14} /> Pause
</button>
<button onClick={save} className="flex items-center gap-2 rounded-md bg-accent px-3 py-2 text-[12px] font-semibold text-white hover:opacity-90">
<button onClick={save} className="app-button app-button-primary px-3 text-[11px]">
<Save size={14} /> Save Settings
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-6">
<div className={`max-w-[760px] space-y-5 ${draft.enabled ? '' : 'opacity-50'}`}>
<section className="rounded-xl border border-border-modal bg-bg-modal/40 p-5">
<div className={`max-w-[760px] space-y-4 ${draft.enabled ? '' : 'opacity-50'}`}>
<section className="app-card p-5">
<div className="mb-5 flex items-center gap-2 font-semibold text-text-primary">
<Clock3 size={17} className="text-accent" /> Timing
</div>
<div className="flex flex-wrap items-end gap-8">
<label className="space-y-2 text-[12px] text-text-secondary">
<span className="block">Start Time</span>
<input type="time" value={draft.startTime} onChange={event => updateDraft('startTime', event.target.value)} disabled={!draft.enabled} className="rounded-md border border-border-modal bg-bg-input px-3 py-2 text-text-primary" />
<input type="time" value={draft.startTime} onChange={event => updateDraft('startTime', event.target.value)} disabled={!draft.enabled} className="app-control px-3 py-2 text-text-primary" />
</label>
<div className="space-y-2">
<label className="flex items-center gap-2 text-[12px] text-text-secondary">
<input type="checkbox" checked={draft.stopTimeEnabled} onChange={event => updateDraft('stopTimeEnabled', event.target.checked)} disabled={!draft.enabled} className="accent-accent" />
Stop Time
</label>
<input type="time" value={draft.stopTime} onChange={event => updateDraft('stopTime', event.target.value)} disabled={!draft.enabled || !draft.stopTimeEnabled} className="rounded-md border border-border-modal bg-bg-input px-3 py-2 text-text-primary disabled:opacity-50" />
<input type="time" value={draft.stopTime} onChange={event => updateDraft('stopTime', event.target.value)} disabled={!draft.enabled || !draft.stopTimeEnabled} className="app-control px-3 py-2 text-text-primary disabled:opacity-50" />
</div>
</div>
@@ -201,7 +201,7 @@ export default function SchedulerView() {
)}
</section>
<section className="rounded-xl border border-border-modal bg-bg-modal/40 p-5">
<section className="app-card p-5">
<div className="mb-4 flex items-center gap-2 font-semibold text-text-primary">
<List size={17} className="text-accent" /> Queues to Schedule
</div>
@@ -212,7 +212,7 @@ export default function SchedulerView() {
</label>
</section>
<section className="rounded-xl border border-border-modal bg-bg-modal/40 p-5">
<section className="app-card p-5">
<div className="mb-2 flex items-center gap-2 font-semibold text-text-primary">
<Power size={17} className="text-accent" /> After Completion
</div>
@@ -238,14 +238,14 @@ export default function SchedulerView() {
</div>
{isMac && (
<section className="mt-5 max-w-[760px] rounded-xl border border-border-modal bg-bg-modal/40 p-5">
<section className="app-card mt-4 max-w-[760px] p-5">
<div className="mb-2 flex items-center gap-2 font-semibold text-text-primary">
<LockKeyhole size={17} className="text-accent" /> System Permissions
</div>
<p className="mb-4 text-[12px] text-text-muted">Sleep, restart, and shut down require macOS Automation permission for Finder.</p>
<div className="flex gap-2">
<button onClick={requestPermission} className="rounded-md bg-accent px-3 py-2 text-[12px] font-semibold text-white">Grant Permission</button>
<button onClick={() => invoke('open_automation_settings')} className="rounded-md border border-border-modal bg-bg-input px-3 py-2 text-[12px] text-text-primary">Open Settings</button>
<button onClick={requestPermission} className="app-button app-button-primary px-3 text-[11px]">Grant Permission</button>
<button onClick={() => invoke('open_automation_settings')} className="app-button px-3 text-[11px]">Open Settings</button>
</div>
{permissionMessage && <p className="mt-3 text-[11px] text-text-muted">{permissionMessage}</p>}
</section>
@@ -253,7 +253,7 @@ export default function SchedulerView() {
</div>
{toast && (
<div className="pointer-events-none absolute bottom-7 left-1/2 -translate-x-1/2 rounded-full border border-border-modal bg-bg-modal px-4 py-2 text-[12px] font-medium text-text-primary shadow-xl">
<div className="app-toast pointer-events-none absolute bottom-7 left-1/2 -translate-x-1/2 px-4 py-2 text-[12px] font-medium">
{toast}
</div>
)}
+68 -30
View File
@@ -21,6 +21,19 @@ const settingsTabs: { type: SettingsTab; label: string; icon: typeof Download }[
{ type: 'about', label: 'About', icon: Info },
];
interface AvailableReleaseUpdate {
version: string;
tag_name: string;
title: string;
release_notes: string;
release_url: string;
published_at: string | null;
}
type ReleaseCheckOutcome =
| { type: 'UpdateAvailable'; update: AvailableReleaseUpdate }
| { type: 'UpToDate'; latest_version: string; local_version: string };
export default function SettingsView() {
const settings = useSettingsStore();
const activeTab = settings.activeSettingsTab;
@@ -45,6 +58,7 @@ export default function SettingsView() {
// Toast notifications
const [toastMessage, setToastMessage] = useState('');
const [isCheckingForUpdates, setIsCheckingForUpdates] = useState(false);
useEffect(() => {
if (toastMessage) {
@@ -78,6 +92,29 @@ export default function SettingsView() {
setToastMessage(msg);
};
const handleCheckForUpdates = async () => {
if (isCheckingForUpdates) return;
setIsCheckingForUpdates(true);
showToast('Checking for updates...');
try {
const result = await invoke<ReleaseCheckOutcome>('check_for_updates');
if (result.type === 'UpToDate') {
showToast(`Firelink ${result.latest_version} is up to date`);
} else if (result.type === 'UpdateAvailable') {
showToast(`Firelink ${result.update.version} is available`);
} else {
showToast('The update check returned an unexpected response');
}
} catch (error) {
showToast(`Update check failed: ${String(error)}`);
} finally {
setIsCheckingForUpdates(false);
}
};
const handleBrowseCategory = async (category: string) => {
const currentPath = (settings.downloadDirectories || {})[category] || '';
try {
@@ -157,33 +194,34 @@ export default function SettingsView() {
return (
<button
type="button"
data-active={active}
onClick={() => settings.setActiveSettingsTab(type)}
className={`flex min-w-0 flex-1 flex-col items-center justify-center rounded-lg px-1 py-2 text-center cursor-default transition-colors ${
className={`settings-tab-button flex min-w-0 flex-1 flex-col items-center justify-center rounded-lg px-1 py-2 text-center cursor-default transition-colors ${
active
? 'bg-accent text-white'
: 'text-text-primary hover:bg-item-hover'
? 'text-text-primary'
: 'text-text-secondary hover:bg-item-hover hover:text-text-primary'
}`}
>
<Icon size={16} strokeWidth={2} />
<Icon size={15} strokeWidth={active ? 2.2 : 1.8} className={active ? 'text-accent' : ''} />
<span className="settings-tab-label mt-1 w-full whitespace-nowrap font-medium">{label}</span>
</button>
);
};
return (
<div className="flex-1 flex flex-col bg-main-bg relative h-full overflow-hidden">
<div className="settings-view flex-1 flex flex-col relative h-full overflow-hidden">
<WindowDragRegion />
{/* Toast Notification */}
{toastMessage && (
<div className="absolute top-4 left-1/2 -translate-x-1/2 bg-accent text-white text-[13px] font-medium py-2 px-4 rounded-full shadow-lg z-50 animate-bounce">
<div className="app-toast absolute top-4 left-1/2 -translate-x-1/2 z-50 px-4 py-2 text-[12px] font-medium">
{toastMessage}
</div>
)}
{/* SwiftUI SettingsPaneContainer-style horizontal tab strip */}
<div className="border-b border-border-color">
<div className="flex items-stretch gap-1 px-8 py-4">
<div className="settings-toolbar">
<div className="settings-tab-strip mx-6 mb-3 flex items-stretch gap-1 rounded-xl p-1">
{settingsTabs.map(tab => (
<TabButton key={tab.type} {...tab} />
))}
@@ -191,14 +229,14 @@ export default function SettingsView() {
</div>
{/* Content Area */}
<div className="flex-1 overflow-y-auto bg-main-bg">
<div className="w-full p-8">
<h1 className="mb-6 text-[28px] font-semibold tracking-tight text-text-primary">{activeTabLabel}</h1>
<div className="max-w-[720px]">
<div className="settings-scroll flex-1 overflow-y-auto">
<div className="w-full p-7">
<h1 className="mb-6 text-[22px] font-semibold tracking-tight text-text-primary">{activeTabLabel}</h1>
<div className="settings-content max-w-[760px]">
{/* Downloads Pane */}
{activeTab === 'downloads' && (
<div className="space-y-6 max-w-[720px]">
<div className="settings-pane space-y-6 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="grid grid-cols-[180px_1fr] items-center gap-4 text-[13px]">
@@ -288,7 +326,7 @@ export default function SettingsView() {
{/* Look & Feel Pane */}
{activeTab === 'lookandfeel' && (
<div className="space-y-6 max-w-[720px]">
<div className="settings-pane space-y-6 max-w-[760px]">
<h3 className="text-base font-semibold text-text-primary border-b border-border-color pb-2">App Theme</h3>
<div className="grid grid-cols-[180px_1fr] items-start gap-4 text-[13px]">
@@ -378,7 +416,7 @@ export default function SettingsView() {
{/* Network Pane */}
{activeTab === 'network' && (
<div className="space-y-6 max-w-[720px]">
<div className="settings-pane space-y-6 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="grid grid-cols-[180px_1fr] items-start gap-4 text-[13px]">
@@ -456,7 +494,7 @@ export default function SettingsView() {
{/* Locations Pane */}
{activeTab === 'locations' && (
<div className="space-y-6 max-w-[720px]">
<div className="settings-pane space-y-6 max-w-[760px]">
<h3 className="text-base font-bold text-text-primary border-b border-border-color/30 pb-2">Download Directories</h3>
<label className="flex items-start gap-3 cursor-default select-none text-[13px] text-text-secondary">
@@ -529,7 +567,7 @@ export default function SettingsView() {
{/* Site Logins Pane */}
{activeTab === 'sitelogins' && (
<div className="space-y-6 max-w-[720px]">
<div className="settings-pane space-y-6 max-w-[760px]">
<h3 className="text-base font-bold text-text-primary border-b border-border-color/30 pb-2">Site Credentials</h3>
{/* Site Logins List */}
@@ -618,7 +656,7 @@ export default function SettingsView() {
{/* Power Pane */}
{activeTab === 'power' && (
<div className="space-y-6 max-w-[720px]">
<div className="settings-pane space-y-6 max-w-[760px]">
<h3 className="text-base font-bold text-text-primary border-b border-border-color/30 pb-2">Power Management</h3>
<label className="flex items-start gap-3 cursor-default select-none text-[13px] text-text-secondary">
@@ -638,7 +676,7 @@ export default function SettingsView() {
{/* Engine Pane */}
{activeTab === 'engine' && (
<div className="space-y-6 max-w-[720px]">
<div className="settings-pane space-y-6 max-w-[760px]">
<h3 className="text-base font-bold text-text-primary border-b border-border-color/30 pb-2">Media Downloader & Engines</h3>
<div className="space-y-4">
@@ -702,7 +740,7 @@ export default function SettingsView() {
{/* Integrations Pane */}
{activeTab === 'integrations' && (
<div className="space-y-6 max-w-[720px]">
<div className="settings-pane space-y-6 max-w-[760px]">
<div className="flex items-center gap-3 border-b border-border-color/30 pb-3">
<Puzzle size={28} className="text-orange-500" />
<div>
@@ -794,7 +832,7 @@ export default function SettingsView() {
{/* About Pane */}
{activeTab === 'about' && (
<div className="space-y-6 max-w-[720px]">
<div className="settings-pane space-y-6 max-w-[760px]">
{/* 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" />
@@ -817,16 +855,16 @@ export default function SettingsView() {
<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"
onClick={handleCheckForUpdates}
disabled={isCheckingForUpdates}
className="app-button px-4 text-xs disabled:opacity-50"
>
Check Now
{isCheckingForUpdates ? (
<>
<RefreshCw size={13} className="animate-spin" />
Checking...
</>
) : 'Check Now'}
</button>
</div>
<label className="p-4 flex items-center justify-between cursor-default">
+30 -26
View File
@@ -63,18 +63,19 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
return (
<button
type="button"
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 ${
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 ${
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'
? 'bg-item-selected text-text-primary font-semibold'
: 'text-text-secondary hover:bg-item-hover hover:text-text-primary'
}`}
onClick={() => onSelectFilter(filter)}
>
<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} />
<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} />
<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 transition-colors ${
isSelected ? 'bg-black/20 text-white' : 'bg-item-hover text-text-secondary group-hover:bg-item-hover'
isSelected ? 'bg-accent/15 text-accent' : 'bg-item-hover text-text-muted'
}`}>
{getCount(filter)}
</span>
@@ -130,19 +131,20 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
return (
<button
type="button"
data-active={isSelected}
onContextMenu={e => handleQueueContextMenu(e, queue.id)}
onClick={() => onSelectFilter(filterId)}
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 ${
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 ${
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'
? '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-white' : 'text-text-secondary group-hover:text-accent'}`} strokeWidth={isSelected ? 2.5 : 2} />
<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} />
<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 transition-colors ${
isSelected ? 'bg-black/20 text-white' : 'bg-item-hover text-text-secondary group-hover:bg-item-hover'
isSelected ? 'bg-accent/15 text-accent' : 'bg-item-hover text-text-muted'
}`}>
{getCount(filterId)}
</span>
@@ -156,12 +158,13 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
return (
<button
type="button"
data-active={isSelected}
onClick={() => setActiveView(view)}
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'
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 ${
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-white' : 'text-text-secondary group-hover:text-accent'}`} strokeWidth={isSelected ? 2.5 : 2} />
<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} />
<span>{label}</span>
</button>
);
@@ -171,16 +174,16 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
<aside className="w-full h-full flex flex-col relative shrink-0">
<WindowDragRegion />
<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>
<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>
<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-5">
<div className="text-[11px] font-bold tracking-wider text-text-muted/70 uppercase px-3 mb-2">Folders</div>
<section className="mb-4">
<div className="text-[9px] font-bold tracking-[0.14em] text-text-muted uppercase px-3.5 mb-1.5">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 +193,8 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
<NavItem icon={FileQuestion} label="Other" filter="Other" />
</section>
<section className="mb-5">
<div className="text-[11px] font-bold tracking-wider text-text-muted/70 uppercase px-3 mb-2">Queues</div>
<section className="mb-4">
<div className="text-[9px] font-bold tracking-[0.14em] text-text-muted uppercase px-3.5 mb-1.5">Queues</div>
{queues.map(queue => (
<QueueItem key={queue.id} queue={queue} />
))}
@@ -216,7 +219,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
<button
type="button"
onClick={() => { setIsAddingQueue(true); setNewQueueName(''); }}
className="flex w-full items-center px-2.5 py-1.5 rounded-lg text-[13px] text-text-secondary hover:bg-item-hover cursor-default transition-colors"
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"
>
<Plus className="w-4 h-4 mr-2 shrink-0" strokeWidth={2} />
<span className="truncate">Add new queue</span>
@@ -225,23 +228,24 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
</section>
<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>
<div className="text-[9px] font-bold tracking-[0.14em] text-text-muted uppercase px-3.5 mb-1.5">Tools</div>
<ToolItem icon={CalendarClock} label="Scheduler" view="scheduler" />
<ToolItem icon={Gauge} label="Speed Limiter" view="speedLimiter" />
</section>
</div>
<div className="shrink-0 border-t border-border-color bg-sidebar-glass px-2 py-2 backdrop-blur-xl">
<div className="shrink-0 border-t border-border-color bg-sidebar-bg px-3 py-2">
<button
type="button"
data-active={activeView === 'settings'}
onClick={() => setActiveView('settings')}
className={`flex w-full items-center px-2.5 py-2 rounded-lg text-[13px] text-left cursor-default transition-colors ${
className={`sidebar-nav-item flex h-8 w-full items-center px-3.5 rounded-lg text-[12px] text-left cursor-default transition-colors ${
activeView === 'settings'
? 'bg-accent text-white font-medium'
: 'text-text-primary hover:bg-item-hover'
? '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-white' : 'text-text-secondary'}`} strokeWidth={activeView === 'settings' ? 2.25 : 2} />
<Settings className={`w-4 h-4 mr-2 ${activeView === 'settings' ? 'text-accent' : 'text-text-muted'}`} strokeWidth={activeView === 'settings' ? 2.25 : 1.8} />
<span>Settings</span>
</button>
</div>
@@ -58,8 +58,8 @@ export default function SpeedLimiterView() {
<div className="flex-1 flex h-full flex-col overflow-hidden bg-main-bg">
<WindowDragRegion />
<div className="flex items-center gap-4 border-b border-border-color px-6 pb-5">
<label className="flex items-center gap-3 text-xl font-bold text-text-primary">
<div className="flex items-center gap-3 border-b border-border-color px-6 pb-4">
<label className="flex items-center gap-3 text-[17px] font-semibold tracking-tight text-text-primary">
<input type="checkbox" checked={enabled} onChange={event => setEnabled(event.target.checked)} className="h-4 w-4 accent-accent" />
Speed Limiter
</label>
@@ -68,13 +68,13 @@ export default function SpeedLimiterView() {
}`}>
{enabled ? `${value} ${unit}` : 'Unlimited'}
</span>
<button onClick={save} className="ml-auto flex items-center gap-2 rounded-md bg-accent px-3 py-2 text-[12px] font-semibold text-white hover:opacity-90">
<button onClick={save} className="app-button app-button-primary ml-auto px-3 text-[11px]">
<Save size={14} /> Save Limit
</button>
</div>
<div className="flex-1 overflow-y-auto p-6">
<section className={`max-w-[720px] rounded-xl border border-border-modal bg-bg-modal/40 p-5 ${enabled ? '' : 'opacity-50'}`}>
<section className={`app-card max-w-[720px] p-5 ${enabled ? '' : 'opacity-50'}`}>
<div className="mb-2 flex items-center gap-2 font-semibold text-text-primary">
<Gauge size={18} className="text-accent" /> Global Speed Limit
</div>
@@ -90,7 +90,7 @@ export default function SpeedLimiterView() {
value={value}
disabled={!enabled}
onChange={event => setValue(Math.max(1, Number(event.target.value) || 1))}
className="w-28 rounded-md border border-border-modal bg-bg-input px-3 py-2 text-right font-mono text-text-primary focus:border-accent focus:outline-none"
className="app-control w-28 px-3 py-2 text-right font-mono"
/>
<div className="flex rounded-md border border-border-modal bg-bg-input p-1">
{(['KB/s', 'MB/s'] as SpeedUnit[]).map(option => (
@@ -120,7 +120,7 @@ export default function SpeedLimiterView() {
type="button"
disabled={!enabled}
onClick={() => preset(presetValue)}
className="rounded-md border border-border-modal bg-bg-input px-4 py-2 text-[12px] font-medium text-text-primary hover:bg-item-hover disabled:cursor-default"
className="app-button px-4 text-[12px] disabled:opacity-50"
>
{presetValue} MB/s
</button>
@@ -130,7 +130,7 @@ export default function SpeedLimiterView() {
</div>
{toast && (
<div className="pointer-events-none absolute bottom-7 left-1/2 -translate-x-1/2 rounded-full border border-border-modal bg-bg-modal px-4 py-2 text-[12px] font-medium text-text-primary shadow-xl">
<div className="app-toast pointer-events-none absolute bottom-7 left-1/2 -translate-x-1/2 px-4 py-2 text-[12px] font-medium">
{toast}
</div>
)}
+259 -32
View File
@@ -1,4 +1,3 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
@import "tailwindcss";
:root {
@@ -17,6 +16,9 @@
--bg-modal: 0 0% 100%;
--bg-input: 0 0% 95%;
--border-modal: 0 0% 85%;
--surface-raised: 0 0% 100%;
--surface-overlay: 0 0% 100% / 0.96;
--shadow-color: 220 30% 10% / 0.14;
}
.theme-light {
@@ -34,23 +36,29 @@
--bg-modal: 0 0% 100%;
--bg-input: 0 0% 0% / 0.04;
--border-modal: 0 0% 85%;
--surface-raised: 0 0% 100%;
--surface-overlay: 0 0% 100% / 0.96;
--shadow-color: 220 30% 10% / 0.14;
}
.theme-dark {
color-scheme: dark;
--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;
--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 {
@@ -68,6 +76,9 @@
--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 {
@@ -85,6 +96,9 @@
--bg-modal: 220 16% 20%;
--bg-input: 0 0% 0% / 0.4;
--border-modal: 0 0% 100% / 0.1;
--surface-raised: 220 16% 18%;
--surface-overlay: 220 16% 20% / 0.96;
--shadow-color: 220 30% 5% / 0.42;
}
@theme {
@@ -102,12 +116,22 @@
--color-bg-modal: hsl(var(--bg-modal));
--color-bg-input: hsl(var(--bg-input));
--color-border-modal: hsl(var(--border-modal));
--color-surface-raised: hsl(var(--surface-raised));
--color-surface-overlay: hsl(var(--surface-overlay));
--color-bg-context-menu: hsl(var(--surface-overlay));
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
--font-sans: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Inter, Roboto, Helvetica, Arial, sans-serif;
}
@layer base {
html,
body {
width: 100%;
height: 100%;
}
body {
margin: 0;
background-color: var(--color-main-bg);
color: var(--color-text-primary);
font-family: var(--font-sans);
@@ -121,10 +145,26 @@
}
* {
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
}
button,
input,
select,
textarea {
font: inherit;
}
button {
-webkit-tap-highlight-color: transparent;
}
button:disabled {
cursor: default;
}
input, textarea {
user-select: auto;
-webkit-user-select: auto;
@@ -132,7 +172,7 @@
}
:focus-visible {
outline: 2px solid hsl(var(--accent-color));
outline: 2px solid hsl(var(--accent-color) / 0.72);
outline-offset: 2px;
}
@@ -154,21 +194,193 @@
::-webkit-scrollbar-thumb:hover {
background: var(--color-text-muted);
}
::selection {
background: hsl(var(--accent-color) / 0.28);
color: var(--color-text-primary);
}
}
@layer components {
.app-surface {
background: hsl(var(--surface-raised));
border: 1px solid hsl(var(--border-color));
box-shadow:
0 1px 0 hsl(0 0% 100% / 0.025) inset,
0 8px 24px hsl(var(--shadow-color));
}
.app-card {
background: hsl(var(--bg-modal));
border: 1px solid hsl(var(--border-color));
border-radius: 12px;
box-shadow:
0 1px 0 hsl(0 0% 100% / 0.02) inset,
0 4px 14px hsl(var(--shadow-color));
}
.app-control {
min-height: 32px;
border: 1px solid hsl(var(--border-modal));
border-radius: 7px;
background: hsl(var(--bg-input));
color: hsl(var(--text-primary));
transition: border-color 140ms ease, background-color 140ms ease, box-shadow 140ms ease;
}
.app-control:hover:not(:disabled) {
border-color: hsl(var(--text-muted) / 0.55);
}
.app-control:focus {
border-color: hsl(var(--accent-color) / 0.7);
box-shadow: 0 0 0 3px hsl(var(--accent-color) / 0.12);
outline: none;
}
.app-button {
display: inline-flex;
min-height: 32px;
align-items: center;
justify-content: center;
gap: 7px;
border: 1px solid hsl(var(--border-modal));
border-radius: 7px;
background: hsl(var(--bg-input));
color: hsl(var(--text-primary));
font-weight: 600;
transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease, transform 100ms ease;
}
.app-button:hover:not(:disabled) {
border-color: hsl(var(--text-muted) / 0.48);
background: hsl(var(--item-hover));
}
.app-button:active:not(:disabled) {
transform: translateY(1px);
}
.app-button-primary {
border-color: hsl(var(--accent-color) / 0.72);
background: hsl(var(--accent-color));
color: white;
box-shadow: 0 5px 16px hsl(var(--accent-color) / 0.16);
}
.app-button-primary:hover:not(:disabled) {
border-color: hsl(var(--accent-color));
background: hsl(var(--accent-color) / 0.9);
}
.app-icon-button {
display: inline-flex;
width: 32px;
height: 32px;
align-items: center;
justify-content: center;
border-radius: 7px;
color: hsl(var(--text-secondary));
transition: background-color 140ms ease, color 140ms ease, transform 100ms ease;
}
.app-icon-button:hover:not(:disabled) {
background: hsl(var(--item-hover));
color: hsl(var(--text-primary));
}
.app-icon-button:active:not(:disabled) {
transform: scale(0.96);
}
.app-modal-backdrop {
background: hsl(0 0% 0% / 0.58);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
animation: fade-in 150ms ease-out;
}
.app-modal {
background: hsl(var(--surface-overlay));
border: 1px solid hsl(var(--border-modal));
border-radius: 14px;
box-shadow:
0 1px 0 hsl(0 0% 100% / 0.05) inset,
0 24px 80px hsl(var(--shadow-color));
animation: modal-in 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.app-toast {
border: 1px solid hsl(var(--border-modal));
border-radius: 999px;
background: hsl(var(--surface-overlay));
color: hsl(var(--text-primary));
box-shadow: 0 12px 32px hsl(var(--shadow-color));
animation: toast-in 180ms ease-out;
}
.app-shell {
background: hsl(var(--main-bg));
}
.app-sidebar {
background: hsl(var(--sidebar-bg));
box-shadow: inset -1px 0 hsl(var(--border-color));
}
.app-workspace {
background: hsl(var(--main-bg));
}
.app-statusbar {
background: hsl(var(--surface-raised));
box-shadow: 0 1px 0 hsl(0 0% 100% / 0.02) inset;
}
.sidebar-nav-item[data-active="true"] {
box-shadow: inset 0 0 0 1px hsl(var(--accent-color) / 0.16);
}
.settings-view,
.settings-scroll {
background: hsl(var(--main-bg));
}
.settings-toolbar {
background: hsl(var(--sidebar-bg));
border-bottom: 1px solid hsl(var(--border-color));
}
.settings-tab-strip {
background: hsl(var(--bg-input));
border: 1px solid hsl(var(--border-color));
box-shadow: 0 1px 0 hsl(0 0% 100% / 0.02) inset;
}
.settings-tab-button[data-active="true"] {
background: hsl(var(--item-selected));
box-shadow: inset 0 0 0 1px hsl(var(--accent-color) / 0.14);
}
.settings-pane {
padding: 22px;
border: 1px solid hsl(var(--border-color));
border-radius: 12px;
background: hsl(var(--surface-raised));
box-shadow:
0 1px 0 hsl(0 0% 100% / 0.02) inset,
0 5px 18px hsl(var(--shadow-color));
}
}
/* Glassmorphism Classes */
.glass-panel {
background: var(--color-sidebar-glass);
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);
background: hsl(var(--bg-modal));
border: 1px solid var(--color-border-color);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
box-shadow: 0 6px 20px hsl(var(--shadow-color));
}
/* Base App Background Gradient - Removed for Modern Minimalist Look */
@@ -187,20 +399,35 @@
}
}
/* Micro Animations */
@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-3px); }
100% { transform: translateY(0px); }
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.animate-float {
animation: float 4s ease-in-out infinite;
@keyframes modal-in {
from { opacity: 0; transform: translateY(6px) scale(0.985); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes toast-in {
from { opacity: 0; transform: translate(-50%, 6px); }
to { opacity: 1; transform: translate(-50%, 0); }
}
.hover-lift {
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
transition: transform 120ms ease, background-color 140ms ease, border-color 140ms ease;
}
.hover-lift:hover {
transform: translateY(-1px) scale(1.01);
transform: translateY(-1px);
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}