mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
feat(desktop): modernize dark theme and update flow
This commit is contained in:
Generated
+2
-2
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"name": "tauri-app",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "0.7.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Generated
+1
-1
@@ -3928,7 +3928,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-app"
|
||||
version = "0.1.0"
|
||||
version = "0.7.3"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
"keyring",
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user