mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-07 01:44:01 +00:00
feat(desktop): align UI with SwiftUI
This commit is contained in:
@@ -8,6 +8,7 @@ import { listen } from "@tauri-apps/api/event";
|
||||
import { useDownloadStore } from "./store/useDownloadStore";
|
||||
import { useSettingsStore } from "./store/useSettingsStore";
|
||||
import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification';
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
function App() {
|
||||
const [filter, setFilter] = useState<SidebarFilter>('all');
|
||||
@@ -16,11 +17,17 @@ function App() {
|
||||
const isSidebarVisible = useSettingsStore(state => state.isSidebarVisible);
|
||||
const activeView = useSettingsStore(state => state.activeView);
|
||||
const appFontSize = useSettingsStore(state => state.appFontSize);
|
||||
const showDockBadge = useSettingsStore(state => state.showDockBadge);
|
||||
const activeDownloadCount = useDownloadStore(state => state.downloads.filter(download => download.status === 'downloading').length);
|
||||
|
||||
useEffect(() => {
|
||||
window.document.documentElement.setAttribute('data-font-size', appFontSize);
|
||||
}, [appFontSize]);
|
||||
|
||||
useEffect(() => {
|
||||
invoke('update_dock_badge', { count: showDockBadge ? activeDownloadCount : 0 }).catch(() => {});
|
||||
}, [showDockBadge, activeDownloadCount]);
|
||||
|
||||
useEffect(() => {
|
||||
// Request notification permissions
|
||||
const initNotifications = async () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SidebarFilter } from './Sidebar';
|
||||
import { Play, Pause, Plus, Trash2, FileText, Image as ImageIcon, Music, Film, Box, Archive, FileQuestion, MoreVertical, PanelLeft } from 'lucide-react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { homeDir } from '@tauri-apps/api/path';
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { WindowDragRegion } from './WindowDragRegion';
|
||||
|
||||
interface DownloadTableProps {
|
||||
filter: SidebarFilter;
|
||||
@@ -18,7 +18,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
||||
const getPaddingY = () => {
|
||||
switch (listRowDensity) {
|
||||
case 'compact': return 'py-1';
|
||||
case 'spacious': return 'py-4';
|
||||
case 'relaxed': return 'py-4';
|
||||
default: return 'py-3';
|
||||
}
|
||||
};
|
||||
@@ -103,58 +103,54 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col bg-main-bg h-full relative">
|
||||
<div className="glass-panel shrink-0 border-b border-border-color/60">
|
||||
<WindowDragRegion className={!isSidebarVisible ? 'pl-24' : ''} />
|
||||
|
||||
{/* Modern Toolbar */}
|
||||
<div
|
||||
className={`flex px-6 py-4 items-center glass-panel z-10 sticky top-0 ${!isSidebarVisible ? 'pl-24' : ''}`}
|
||||
onPointerDown={(e) => {
|
||||
if (e.button === 0 && (e.target as HTMLElement).closest('.no-drag') === null) {
|
||||
getCurrentWindow().startDragging();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="no-drag mr-4 p-1.5 rounded-md text-text-secondary hover:text-text-primary hover:bg-item-hover transition-colors"
|
||||
title="Toggle Sidebar"
|
||||
>
|
||||
<PanelLeft size={18} strokeWidth={2} />
|
||||
</button>
|
||||
<h2 className="text-[15px] font-bold mr-auto text-text-primary tracking-tight cursor-default">{getFilterTitle()}</h2>
|
||||
{/* Download Toolbar */}
|
||||
<div className={`flex px-6 pb-4 items-center ${!isSidebarVisible ? 'pl-24' : ''}`}>
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="mr-4 p-1.5 rounded-md text-text-secondary hover:text-text-primary hover:bg-item-hover transition-colors"
|
||||
title="Toggle Sidebar"
|
||||
>
|
||||
<PanelLeft size={18} strokeWidth={2} />
|
||||
</button>
|
||||
<h2 className="text-[15px] font-bold mr-auto text-text-primary tracking-tight cursor-default">{getFilterTitle()}</h2>
|
||||
|
||||
<div className="flex gap-1.5 items-center no-drag">
|
||||
<button
|
||||
onClick={() => toggleAddModal(true)}
|
||||
className="p-1.5 rounded-md text-text-secondary hover:text-white hover:bg-item-hover transition-all duration-200"
|
||||
title="Add Download"
|
||||
>
|
||||
<Plus size={16} strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
filteredDownloads.filter(d => d.status === 'paused').forEach(d => handleResume(d));
|
||||
}}
|
||||
className="p-1.5 rounded-md text-text-secondary hover:text-white hover:bg-item-hover transition-all duration-200"
|
||||
title="Resume All"
|
||||
>
|
||||
<Play size={16} fill="currentColor" className="opacity-90" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
filteredDownloads.filter(d => d.status === 'downloading').forEach(d => handlePause(d.id));
|
||||
}}
|
||||
className="p-1.5 rounded-md text-text-secondary hover:text-white hover:bg-item-hover transition-all duration-200"
|
||||
title="Pause All"
|
||||
>
|
||||
<Pause size={16} fill="currentColor" className="opacity-90" />
|
||||
</button>
|
||||
<button
|
||||
onClick={clearFinished}
|
||||
className="p-1.5 rounded-md text-text-secondary hover:text-red-400 hover:bg-item-hover transition-all duration-200"
|
||||
title="Clear Finished"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
<div className="flex gap-1.5 items-center">
|
||||
<button
|
||||
onClick={() => toggleAddModal(true)}
|
||||
className="p-1.5 rounded-md text-text-secondary hover:text-white hover:bg-item-hover transition-all duration-200"
|
||||
title="Add Download"
|
||||
>
|
||||
<Plus size={16} strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
filteredDownloads.filter(d => d.status === 'paused').forEach(d => handleResume(d));
|
||||
}}
|
||||
className="p-1.5 rounded-md text-text-secondary hover:text-white hover:bg-item-hover transition-all duration-200"
|
||||
title="Resume All"
|
||||
>
|
||||
<Play size={16} fill="currentColor" className="opacity-90" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
filteredDownloads.filter(d => d.status === 'downloading').forEach(d => handlePause(d.id));
|
||||
}}
|
||||
className="p-1.5 rounded-md text-text-secondary hover:text-white hover:bg-item-hover transition-all duration-200"
|
||||
title="Pause All"
|
||||
>
|
||||
<Pause size={16} fill="currentColor" className="opacity-90" />
|
||||
</button>
|
||||
<button
|
||||
onClick={clearFinished}
|
||||
className="p-1.5 rounded-md text-text-secondary hover:text-red-400 hover:bg-item-hover transition-all duration-200"
|
||||
title="Clear Finished"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSettingsStore } from '../store/useSettingsStore';
|
||||
import { SettingsTab, useSettingsStore } from '../store/useSettingsStore';
|
||||
import {
|
||||
Download, Palette, Globe, Folder, Key,
|
||||
Moon, Terminal, Puzzle, Info, Plus, Trash2, Copy, RefreshCw
|
||||
} from 'lucide-react';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { WindowDragRegion } from './WindowDragRegion';
|
||||
|
||||
type TabType = 'downloads' | 'lookandfeel' | 'network' | 'locations' | 'sitelogins' | 'power' | 'engine' | 'integrations' | 'about';
|
||||
const settingsTabs: { type: SettingsTab; label: string; icon: typeof Download }[] = [
|
||||
{ type: 'downloads', label: 'Downloads', icon: Download },
|
||||
{ type: 'lookandfeel', label: 'Look and feel', icon: Palette },
|
||||
{ type: 'network', label: 'Network', icon: Globe },
|
||||
{ type: 'locations', label: 'Locations', icon: Folder },
|
||||
{ type: 'sitelogins', label: 'Site Logins', icon: Key },
|
||||
{ type: 'power', label: 'Power', icon: Moon },
|
||||
{ type: 'engine', label: 'Engine', icon: Terminal },
|
||||
{ type: 'integrations', label: 'Integrations', icon: Puzzle },
|
||||
{ type: 'about', label: 'About', icon: Info },
|
||||
];
|
||||
|
||||
export default function SettingsView() {
|
||||
const settings = useSettingsStore();
|
||||
const [activeTab, setActiveTab] = useState<TabType>('downloads');
|
||||
const activeTab = settings.activeSettingsTab;
|
||||
|
||||
// Local state for versions
|
||||
const [aria2Version, setAria2Version] = useState('Checking...');
|
||||
@@ -118,57 +129,55 @@ export default function SettingsView() {
|
||||
showToast("Token copied to clipboard!");
|
||||
};
|
||||
|
||||
const TabButton = ({ type, icon: Icon, label }: { type: TabType; icon: any; label: string }) => {
|
||||
const activeTabLabel = settingsTabs.find(tab => tab.type === activeTab)?.label ?? 'Downloads';
|
||||
|
||||
const TabButton = ({ type, icon: Icon, label }: { type: SettingsTab; icon: typeof Download; label: string }) => {
|
||||
const active = activeTab === type;
|
||||
return (
|
||||
<button
|
||||
onClick={() => setActiveTab(type)}
|
||||
className={`flex flex-col items-center justify-center p-2 rounded-lg transition-all text-center min-w-[76px] cursor-default ${
|
||||
type="button"
|
||||
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 ${
|
||||
active
|
||||
? 'bg-blue-600/15 text-blue-500 font-semibold'
|
||||
: 'text-text-secondary hover:bg-item-hover hover:text-text-primary'
|
||||
? 'bg-accent text-white'
|
||||
: 'text-text-primary hover:bg-item-hover'
|
||||
}`}
|
||||
>
|
||||
<Icon size={18} className="mb-1" />
|
||||
<span className="text-[10px] whitespace-nowrap">{label}</span>
|
||||
<Icon size={16} strokeWidth={2} />
|
||||
<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">
|
||||
<WindowDragRegion />
|
||||
|
||||
{/* Toast Notification */}
|
||||
{toastMessage && (
|
||||
<div className="absolute top-4 left-1/2 -translate-x-1/2 bg-blue-600 text-white text-[13px] font-medium py-2 px-4 rounded-full shadow-lg z-50 animate-bounce">
|
||||
<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">
|
||||
{toastMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header (Horizontal Tab Bar) */}
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center justify-between p-4 px-6 border-b border-border-color">
|
||||
<h2 className="text-[15px] font-bold tracking-tight text-text-primary">Preferences</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-3 overflow-x-auto justify-center bg-bg-input/30 border-b border-border-color shadow-sm">
|
||||
<TabButton type="downloads" icon={Download} label="Downloads" />
|
||||
<TabButton type="lookandfeel" icon={Palette} label="Look & Feel" />
|
||||
<TabButton type="network" icon={Globe} label="Network" />
|
||||
<TabButton type="locations" icon={Folder} label="Locations" />
|
||||
<TabButton type="sitelogins" icon={Key} label="Site Logins" />
|
||||
<TabButton type="power" icon={Moon} label="Power" />
|
||||
<TabButton type="engine" icon={Terminal} label="Engine" />
|
||||
<TabButton type="integrations" icon={Puzzle} label="Integrations" />
|
||||
<TabButton type="about" icon={Info} label="About" />
|
||||
{/* SwiftUI SettingsPaneContainer-style horizontal tab strip */}
|
||||
<div className="border-b border-border-color">
|
||||
<div className="flex items-stretch gap-1 px-8 py-4">
|
||||
{settingsTabs.map(tab => (
|
||||
<TabButton key={tab.type} {...tab} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="flex-1 overflow-y-auto p-6 bg-main-bg/10">
|
||||
<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]">
|
||||
|
||||
{/* Downloads Pane */}
|
||||
{activeTab === 'downloads' && (
|
||||
<div className="space-y-6 max-w-xl mx-auto">
|
||||
<div className="space-y-6 max-w-[720px]">
|
||||
<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]">
|
||||
@@ -178,7 +187,7 @@ export default function SettingsView() {
|
||||
type="range" min="1" max="12"
|
||||
value={settings.maxConcurrentDownloads}
|
||||
onChange={(e) => settings.setMaxConcurrentDownloads(Number(e.target.value))}
|
||||
className="flex-1 accent-blue-500"
|
||||
className="flex-1 accent-accent"
|
||||
/>
|
||||
<span className="w-8 text-center font-mono font-bold bg-item-hover px-2 py-1 rounded border border-border-modal text-text-secondary">
|
||||
{settings.maxConcurrentDownloads}
|
||||
@@ -193,7 +202,7 @@ export default function SettingsView() {
|
||||
type="number" min="1" max="16"
|
||||
value={settings.perServerConnections}
|
||||
onChange={(e) => settings.setPerServerConnections(Number(e.target.value))}
|
||||
className="bg-bg-input border border-border-modal rounded-md px-3 py-1.5 w-24 text-text-primary focus:outline-none focus:border-blue-500"
|
||||
className="bg-bg-input border border-border-modal rounded-md px-3 py-1.5 w-24 text-text-primary focus:outline-none focus:border-accent"
|
||||
/>
|
||||
<span className="text-text-muted text-xs">For new downloads (1 to 16)</span>
|
||||
</div>
|
||||
@@ -207,7 +216,7 @@ export default function SettingsView() {
|
||||
value={settings.globalSpeedLimit}
|
||||
onChange={(e) => settings.setGlobalSpeedLimit(e.target.value)}
|
||||
placeholder="Unlimited"
|
||||
className="bg-bg-input border border-border-modal rounded-md px-3 py-1.5 w-32 font-mono text-text-primary focus:outline-none focus:border-blue-500"
|
||||
className="bg-bg-input border border-border-modal rounded-md px-3 py-1.5 w-32 font-mono text-text-primary focus:outline-none focus:border-accent"
|
||||
/>
|
||||
<span className="text-text-muted text-xs">e.g. 500K, 1M, or 0 for unlimited</span>
|
||||
</div>
|
||||
@@ -220,7 +229,7 @@ export default function SettingsView() {
|
||||
type="number" min="0" max="10"
|
||||
value={settings.maxAutomaticRetries}
|
||||
onChange={(e) => settings.setMaxAutomaticRetries(Number(e.target.value))}
|
||||
className="bg-bg-input border border-border-modal rounded-md px-3 py-1.5 w-24 text-text-primary focus:outline-none focus:border-blue-500"
|
||||
className="bg-bg-input border border-border-modal rounded-md px-3 py-1.5 w-24 text-text-primary focus:outline-none focus:border-accent"
|
||||
/>
|
||||
<span className="text-text-muted text-xs">If a connection fails (0 to 10)</span>
|
||||
</div>
|
||||
@@ -232,7 +241,7 @@ export default function SettingsView() {
|
||||
type="checkbox"
|
||||
checked={settings.showNotifications}
|
||||
onChange={(e) => settings.setShowNotifications(e.target.checked)}
|
||||
className="mt-0.5 rounded accent-blue-500"
|
||||
className="mt-0.5 rounded accent-accent"
|
||||
/>
|
||||
<div>
|
||||
<p className="font-semibold text-text-primary">Show notification when download completes</p>
|
||||
@@ -246,7 +255,7 @@ export default function SettingsView() {
|
||||
checked={settings.playCompletionSound && settings.showNotifications}
|
||||
disabled={!settings.showNotifications}
|
||||
onChange={(e) => settings.setPlayCompletionSound(e.target.checked)}
|
||||
className="mt-0.5 rounded accent-blue-500 disabled:opacity-40"
|
||||
className="mt-0.5 rounded accent-accent disabled:opacity-40"
|
||||
/>
|
||||
<div>
|
||||
<p className={`font-semibold ${settings.showNotifications ? 'text-text-primary' : 'text-text-muted'}`}>Play sound when download completes</p>
|
||||
@@ -258,39 +267,47 @@ export default function SettingsView() {
|
||||
|
||||
{/* Look & Feel Pane */}
|
||||
{activeTab === 'lookandfeel' && (
|
||||
<div className="space-y-6 max-w-xl mx-auto">
|
||||
<h3 className="text-base font-bold text-text-primary border-b border-border-color/30 pb-2">Appearance Settings</h3>
|
||||
<div className="space-y-6 max-w-[720px]">
|
||||
<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]">
|
||||
<label className="text-text-secondary font-medium pt-1">App Theme:</label>
|
||||
<label className="text-text-secondary font-medium pt-1">Theme:</label>
|
||||
<div className="space-y-2">
|
||||
{['system', 'dark', 'light', 'dracula', 'nord'].map((t) => (
|
||||
<label key={t} className="flex items-center gap-2 cursor-default select-none text-text-secondary capitalize">
|
||||
{[
|
||||
{ value: 'system', label: 'System Default' },
|
||||
{ value: 'light', label: 'Light' },
|
||||
{ value: 'dark', label: 'Dark' },
|
||||
{ value: 'dracula', label: 'Dracula' },
|
||||
{ value: 'nord', label: 'Nord' },
|
||||
].map(({ value, label }) => (
|
||||
<label key={value} className="flex items-center gap-2 cursor-default select-none text-text-primary">
|
||||
<input
|
||||
type="radio"
|
||||
name="themeRadio"
|
||||
value={t}
|
||||
checked={settings.theme === t}
|
||||
onChange={() => settings.setTheme(t as any)}
|
||||
className="accent-blue-500"
|
||||
value={value}
|
||||
checked={settings.theme === value}
|
||||
onChange={() => settings.setTheme(value as typeof settings.theme)}
|
||||
className="accent-accent"
|
||||
/>
|
||||
{t === 'system' ? 'System Default' : t}
|
||||
{label}
|
||||
</label>
|
||||
))}
|
||||
<p className="text-text-muted text-xs mt-2">Select a color palette for the app's user interface.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-base font-semibold text-text-primary border-b border-border-color pb-2 pt-2">Display</h3>
|
||||
|
||||
<div className="grid grid-cols-[180px_1fr] items-center gap-4 text-[13px]">
|
||||
<label className="text-text-secondary font-medium">Font Size:</label>
|
||||
<select
|
||||
value={settings.appFontSize}
|
||||
onChange={(e) => settings.setAppFontSize(e.target.value as any)}
|
||||
className="bg-bg-input border border-border-modal rounded-lg p-2 text-[13px] text-text-primary focus:outline-none focus:border-blue-500 max-w-[200px]"
|
||||
className="bg-bg-input border border-border-modal rounded-md px-3 py-1.5 text-[13px] text-text-primary focus:outline-none focus:border-accent max-w-[200px]"
|
||||
>
|
||||
<option value="small">Small</option>
|
||||
<option value="standard">Standard</option>
|
||||
<option value="large">Large</option>
|
||||
<option value="extra-large">Extra Large</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -299,33 +316,48 @@ export default function SettingsView() {
|
||||
<select
|
||||
value={settings.listRowDensity}
|
||||
onChange={(e) => settings.setListRowDensity(e.target.value as any)}
|
||||
className="bg-bg-input border border-border-modal rounded-lg p-2 text-[13px] text-text-primary focus:outline-none focus:border-blue-500 max-w-[200px]"
|
||||
className="bg-bg-input border border-border-modal rounded-md px-3 py-1.5 text-[13px] text-text-primary focus:outline-none focus:border-accent max-w-[200px]"
|
||||
>
|
||||
<option value="compact">Compact</option>
|
||||
<option value="standard">Standard</option>
|
||||
<option value="spacious">Spacious</option>
|
||||
<option value="relaxed">Relaxed</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border-color/30 pt-4 space-y-3">
|
||||
<h3 className="text-base font-semibold text-text-primary border-b border-border-color pb-2 pt-2">macOS Integration</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-start gap-3 cursor-default select-none text-[13px] text-text-secondary">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.showNotifications} // mapped to dock badge placeholder
|
||||
className="mt-0.5 rounded accent-blue-500"
|
||||
checked={settings.showDockBadge}
|
||||
onChange={(e) => settings.setShowDockBadge(e.target.checked)}
|
||||
className="mt-0.5 rounded accent-accent"
|
||||
/>
|
||||
<div>
|
||||
<p className="font-semibold text-text-primary">Show badge on Dock/Taskbar icon</p>
|
||||
<p className="text-text-muted text-xs mt-0.5">Displays the number of active downloads on the icon badge.</p>
|
||||
</div>
|
||||
</label>
|
||||
<label className="flex items-start gap-3 cursor-default select-none text-[13px] text-text-secondary">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.showMenuBarIcon}
|
||||
onChange={(e) => settings.setShowMenuBarIcon(e.target.checked)}
|
||||
className="mt-0.5 rounded accent-accent"
|
||||
/>
|
||||
<div>
|
||||
<p className="font-semibold text-text-primary">Show menu bar icon</p>
|
||||
<p className="text-text-muted text-xs mt-0.5">Provides quick access to downloads and queues from the system menu bar.</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Network Pane */}
|
||||
{activeTab === 'network' && (
|
||||
<div className="space-y-6 max-w-xl mx-auto">
|
||||
<div className="space-y-6 max-w-[720px]">
|
||||
<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]">
|
||||
@@ -336,7 +368,7 @@ export default function SettingsView() {
|
||||
type="radio" name="proxyMode" value="none"
|
||||
checked={settings.proxyMode === 'none'}
|
||||
onChange={() => settings.setProxyMode('none')}
|
||||
className="accent-blue-500"
|
||||
className="accent-accent"
|
||||
/>
|
||||
No proxy
|
||||
</label>
|
||||
@@ -345,7 +377,7 @@ export default function SettingsView() {
|
||||
type="radio" name="proxyMode" value="system"
|
||||
checked={settings.proxyMode === 'system'}
|
||||
onChange={() => settings.setProxyMode('system')}
|
||||
className="accent-blue-500"
|
||||
className="accent-accent"
|
||||
/>
|
||||
Use system proxy
|
||||
</label>
|
||||
@@ -354,7 +386,7 @@ export default function SettingsView() {
|
||||
type="radio" name="proxyMode" value="custom"
|
||||
checked={settings.proxyMode === 'custom'}
|
||||
onChange={() => settings.setProxyMode('custom')}
|
||||
className="accent-blue-500"
|
||||
className="accent-accent"
|
||||
/>
|
||||
Set proxy
|
||||
</label>
|
||||
@@ -393,7 +425,7 @@ export default function SettingsView() {
|
||||
value={settings.customUserAgent}
|
||||
onChange={(e) => settings.setCustomUserAgent(e.target.value)}
|
||||
placeholder="e.g. Mozilla/5.0..."
|
||||
className="bg-bg-input border border-border-modal rounded-md px-3 py-1.5 w-full font-mono text-[11px] text-text-primary focus:outline-none focus:border-blue-500"
|
||||
className="bg-bg-input border border-border-modal rounded-md px-3 py-1.5 w-full font-mono text-[11px] text-text-primary focus:outline-none focus:border-accent"
|
||||
/>
|
||||
<p className="text-text-muted text-xs">Spoofs browser User-Agent to bypass download restrictions. Leave blank for default.</p>
|
||||
</div>
|
||||
@@ -403,7 +435,7 @@ export default function SettingsView() {
|
||||
|
||||
{/* Locations Pane */}
|
||||
{activeTab === 'locations' && (
|
||||
<div className="space-y-6 max-w-xl mx-auto">
|
||||
<div className="space-y-6 max-w-[720px]">
|
||||
<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">
|
||||
@@ -411,7 +443,7 @@ export default function SettingsView() {
|
||||
type="checkbox"
|
||||
checked={settings.askWhereToSaveEachFile}
|
||||
onChange={(e) => settings.setAskWhereToSaveEachFile(e.target.checked)}
|
||||
className="mt-0.5 rounded accent-blue-500"
|
||||
className="mt-0.5 rounded accent-accent"
|
||||
/>
|
||||
<div>
|
||||
<p className="font-semibold text-text-primary">Ask where to save each file before downloading</p>
|
||||
@@ -432,7 +464,7 @@ export default function SettingsView() {
|
||||
/>
|
||||
<button
|
||||
onClick={handleBrowseBulk}
|
||||
className="bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded-md text-xs font-semibold shadow transition-colors"
|
||||
className="bg-accent hover:bg-accent text-white px-3 py-1 rounded-md text-xs font-semibold shadow transition-colors"
|
||||
>
|
||||
Choose Base
|
||||
</button>
|
||||
@@ -476,7 +508,7 @@ export default function SettingsView() {
|
||||
|
||||
{/* Site Logins Pane */}
|
||||
{activeTab === 'sitelogins' && (
|
||||
<div className="space-y-6 max-w-xl mx-auto">
|
||||
<div className="space-y-6 max-w-[720px]">
|
||||
<h3 className="text-base font-bold text-text-primary border-b border-border-color/30 pb-2">Site Credentials</h3>
|
||||
|
||||
{/* Site Logins List */}
|
||||
@@ -549,7 +581,7 @@ export default function SettingsView() {
|
||||
<div className="flex justify-end pt-2">
|
||||
<button
|
||||
onClick={handleAddLogin}
|
||||
className="bg-blue-600 hover:bg-blue-500 text-white px-4 py-1.5 rounded-lg text-xs font-semibold shadow flex items-center gap-1.5"
|
||||
className="bg-accent hover:bg-accent text-white px-4 py-1.5 rounded-lg text-xs font-semibold shadow flex items-center gap-1.5"
|
||||
>
|
||||
<Plus size={14} /> Add Login
|
||||
</button>
|
||||
@@ -560,7 +592,7 @@ export default function SettingsView() {
|
||||
|
||||
{/* Power Pane */}
|
||||
{activeTab === 'power' && (
|
||||
<div className="space-y-6 max-w-xl mx-auto">
|
||||
<div className="space-y-6 max-w-[720px]">
|
||||
<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">
|
||||
@@ -568,7 +600,7 @@ export default function SettingsView() {
|
||||
type="checkbox"
|
||||
checked={settings.preventsSleepWhileDownloading}
|
||||
onChange={(e) => settings.setPreventsSleepWhileDownloading(e.target.checked)}
|
||||
className="mt-0.5 rounded accent-blue-500"
|
||||
className="mt-0.5 rounded accent-accent"
|
||||
/>
|
||||
<div>
|
||||
<p className="font-semibold text-text-primary">Prevent system sleep while downloads are active</p>
|
||||
@@ -580,13 +612,13 @@ export default function SettingsView() {
|
||||
|
||||
{/* Engine Pane */}
|
||||
{activeTab === 'engine' && (
|
||||
<div className="space-y-6 max-w-xl mx-auto">
|
||||
<div className="space-y-6 max-w-[720px]">
|
||||
<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">
|
||||
<div className="border border-border-modal rounded-lg p-4 space-y-3 bg-item-hover/5">
|
||||
<h4 className="text-[13px] font-bold text-text-primary flex items-center gap-2 border-b border-border-modal pb-1">
|
||||
<Terminal size={14} className="text-blue-500" /> Core Downloader (Aria2)
|
||||
<Terminal size={14} className="text-accent" /> Core Downloader (Aria2)
|
||||
</h4>
|
||||
<div className="grid grid-cols-[120px_1fr] text-[13px]">
|
||||
<span className="text-text-secondary">Version:</span>
|
||||
@@ -620,7 +652,7 @@ export default function SettingsView() {
|
||||
<select
|
||||
value={settings.mediaCookieSource}
|
||||
onChange={(e) => settings.setMediaCookieSource(e.target.value as any)}
|
||||
className="bg-bg-input border border-border-modal rounded-lg p-1.5 text-[13px] text-text-primary focus:outline-none focus:border-blue-500"
|
||||
className="bg-bg-input border border-border-modal rounded-lg p-1.5 text-[13px] text-text-primary focus:outline-none focus:border-accent"
|
||||
>
|
||||
<option value="none">None</option>
|
||||
<option value="safari">Safari</option>
|
||||
@@ -638,7 +670,7 @@ export default function SettingsView() {
|
||||
|
||||
{/* Integrations Pane */}
|
||||
{activeTab === 'integrations' && (
|
||||
<div className="space-y-6 max-w-xl mx-auto">
|
||||
<div className="space-y-6 max-w-[720px]">
|
||||
<div className="flex items-center gap-3 border-b border-border-color/30 pb-3">
|
||||
<Puzzle size={28} className="text-orange-500" />
|
||||
<div>
|
||||
@@ -654,8 +686,8 @@ export default function SettingsView() {
|
||||
<div className="border border-border-modal rounded-lg p-4 bg-item-hover/5 flex flex-col justify-between h-[190px]">
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="bg-blue-600/25 text-blue-500 font-bold rounded-full w-5 h-5 flex items-center justify-center text-xs">1</span>
|
||||
<Copy size={16} className="text-blue-500" />
|
||||
<span className="bg-accent/25 text-accent font-bold rounded-full w-5 h-5 flex items-center justify-center text-xs">1</span>
|
||||
<Copy size={16} className="text-accent" />
|
||||
</div>
|
||||
<h4 className="text-[13px] font-bold text-text-primary mb-1">Copy Token</h4>
|
||||
<p className="text-text-muted text-[11px] leading-relaxed">This secure token authorizes your browser extension.</p>
|
||||
@@ -663,7 +695,7 @@ export default function SettingsView() {
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={copyToken}
|
||||
className="w-full bg-blue-600 hover:bg-blue-500 text-white font-medium py-1 px-2 rounded text-[11px] flex items-center justify-center gap-1 shadow transition-colors"
|
||||
className="w-full bg-accent hover:bg-accent text-white font-medium py-1 px-2 rounded text-[11px] flex items-center justify-center gap-1 shadow transition-colors"
|
||||
>
|
||||
<Copy size={11} /> Copy Token
|
||||
</button>
|
||||
@@ -731,7 +763,7 @@ export default function SettingsView() {
|
||||
{/* About Pane */}
|
||||
{activeTab === 'about' && (
|
||||
<div className="space-y-6 max-w-md mx-auto text-center py-6">
|
||||
<div className="w-16 h-16 bg-blue-600 text-white font-extrabold text-2xl flex items-center justify-center rounded-2xl mx-auto shadow-lg shadow-blue-500/20">
|
||||
<div className="w-16 h-16 bg-accent text-white font-extrabold text-2xl flex items-center justify-center rounded-2xl mx-auto shadow-lg shadow-accent/20">
|
||||
FL
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -742,7 +774,7 @@ export default function SettingsView() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border-color/30 pt-4 flex justify-center gap-4 text-xs text-blue-500">
|
||||
<div className="border-t border-border-color/30 pt-4 flex justify-center gap-4 text-xs text-accent">
|
||||
<a href="https://github.com/nimbold/Firelink" target="_blank" rel="noreferrer" className="hover:underline">GitHub Repository</a>
|
||||
<span>•</span>
|
||||
<a href="https://github.com/nimbold/Firelink/issues" target="_blank" rel="noreferrer" className="hover:underline">Report Issues</a>
|
||||
@@ -751,15 +783,8 @@ export default function SettingsView() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-border-modal bg-sidebar-bg/50 flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => settings.setActiveView('downloads')}
|
||||
className="px-5 py-2 rounded-lg text-sm font-medium bg-blue-600 hover:bg-blue-500 text-white shadow-lg shadow-blue-500/20 transition-all active:scale-95"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -2,11 +2,11 @@ import React from 'react';
|
||||
import {
|
||||
Inbox, Zap, CheckCircle2, CircleDashed,
|
||||
Film, Music, FileText, Box, Image as ImageIcon, Archive, FileQuestion,
|
||||
List, CalendarClock, Gauge, Settings
|
||||
List, CalendarClock, Gauge, Settings, Plus
|
||||
} from 'lucide-react';
|
||||
import { useDownloadStore, DownloadCategory } from '../store/useDownloadStore';
|
||||
import { useSettingsStore } from '../store/useSettingsStore';
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { WindowDragRegion } from './WindowDragRegion';
|
||||
|
||||
export type SidebarFilter = 'all' | 'active' | 'completed' | 'unfinished' | DownloadCategory | 'settings';
|
||||
|
||||
@@ -31,49 +31,46 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const NavItem = ({ icon: Icon, label, filter }: { icon: any, label: string, filter: SidebarFilter }) => (
|
||||
<div
|
||||
className={`flex items-center px-3 py-1.5 rounded-md text-[13px] cursor-default transition-colors mb-[2px] ${
|
||||
selectedFilter === filter
|
||||
? 'bg-[#3B66DE] text-white shadow-sm font-medium'
|
||||
: 'text-text-secondary hover:bg-item-hover hover:text-text-primary font-medium'
|
||||
}`}
|
||||
onClick={() => onSelectFilter(filter)}
|
||||
>
|
||||
<Icon className={`w-4 h-4 mr-2.5 ${selectedFilter === filter ? 'opacity-100 text-white' : 'opacity-70'}`} strokeWidth={selectedFilter === filter ? 2.5 : 2} />
|
||||
<span>{label}</span>
|
||||
{getCount(filter) > 0 && (
|
||||
<span className={`ml-auto text-[11px] font-bold px-1.5 py-0.5 rounded-full ${
|
||||
selectedFilter === filter
|
||||
? 'bg-black/20 text-white'
|
||||
: 'bg-item-hover text-text-muted group-hover:bg-black/10'
|
||||
}`}>
|
||||
{getCount(filter)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
const NavItem = ({ icon: Icon, label, filter }: { icon: any, label: string, filter: SidebarFilter }) => {
|
||||
const isSelected = activeView === 'downloads' && selectedFilter === filter;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`group flex w-full items-center px-2.5 py-1.5 rounded-lg text-[13px] text-left cursor-default transition-colors mb-0.5 ${
|
||||
isSelected
|
||||
? 'bg-accent text-white font-medium'
|
||||
: 'text-text-primary hover:bg-item-hover'
|
||||
}`}
|
||||
onClick={() => onSelectFilter(filter)}
|
||||
>
|
||||
<Icon className={`w-4 h-4 mr-2 ${isSelected ? 'text-white' : 'text-text-secondary'}`} strokeWidth={isSelected ? 2.25 : 2} />
|
||||
<span className="truncate">{label}</span>
|
||||
{getCount(filter) > 0 && (
|
||||
<span className={`ml-auto min-w-5 px-1.5 py-0.5 rounded-full text-center text-[10px] leading-none font-semibold ${
|
||||
isSelected ? 'bg-black/20 text-white' : 'bg-item-hover text-text-secondary'
|
||||
}`}>
|
||||
{getCount(filter)}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-[220px] min-w-[190px] max-w-[260px] bg-[#1E1E20] border-r border-border-color flex flex-col p-2.5 pt-8 pb-4 relative shrink-0">
|
||||
<div
|
||||
className="absolute top-0 left-0 right-0 h-10 z-50"
|
||||
data-tauri-drag-region
|
||||
onPointerDown={(e) => {
|
||||
if (e.button === 0) getCurrentWindow().startDragging();
|
||||
}}
|
||||
/>
|
||||
<div className="overflow-y-auto flex-1 flex flex-col hide-scrollbar">
|
||||
<div className="mb-5 shrink-0 mt-2">
|
||||
<div className="text-[10px] font-bold text-text-muted/60 tracking-widest px-3 mb-2">LIBRARY</div>
|
||||
<aside className="w-[220px] min-w-[190px] max-w-[260px] bg-sidebar-bg border-r border-border-color flex flex-col relative shrink-0">
|
||||
<WindowDragRegion />
|
||||
<div className="overflow-y-auto flex-1 px-2 pb-3">
|
||||
<section className="mb-4">
|
||||
<div className="text-[11px] font-semibold text-text-muted px-2.5 mb-1.5">Library</div>
|
||||
<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" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="mb-5 shrink-0">
|
||||
<div className="text-[10px] font-bold text-text-muted/60 tracking-widest px-3 mb-2">FOLDERS</div>
|
||||
<section className="mb-4">
|
||||
<div className="text-[11px] font-semibold text-text-muted px-2.5 mb-1.5">Folders</div>
|
||||
<NavItem icon={Film} label="Video" filter="Video" />
|
||||
<NavItem icon={Music} label="Audio" filter="Audio" />
|
||||
<NavItem icon={FileText} label="Documents" filter="Documents" />
|
||||
@@ -81,39 +78,45 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||
<NavItem icon={ImageIcon} label="Images" filter="Images" />
|
||||
<NavItem icon={Archive} label="Archives" filter="Archives" />
|
||||
<NavItem icon={FileQuestion} label="Other" filter="Other" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="mb-5 shrink-0">
|
||||
<div className="text-[10px] font-bold text-text-muted/60 tracking-widest px-3 mb-2">QUEUES</div>
|
||||
<div className="flex items-center px-3 py-1.5 rounded-md text-[13px] font-medium text-text-secondary hover:bg-item-hover hover:text-text-primary cursor-default transition-colors mb-[2px]">
|
||||
<List className="w-4 h-4 mr-2.5 opacity-70" strokeWidth={2} />
|
||||
<section className="mb-4">
|
||||
<div className="text-[11px] font-semibold text-text-muted px-2.5 mb-1.5">Queues</div>
|
||||
<div className="flex items-center px-2.5 py-1.5 rounded-lg text-[13px] text-text-primary hover:bg-item-hover cursor-default transition-colors mb-0.5">
|
||||
<List className="w-4 h-4 mr-2 text-text-secondary" strokeWidth={2} />
|
||||
<span>Main Queue</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center px-2.5 py-1.5 rounded-lg text-[13px] text-text-secondary hover:bg-item-hover cursor-default transition-colors">
|
||||
<Plus className="w-4 h-4 mr-2" strokeWidth={2} />
|
||||
<span>Add new queue</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="shrink-0 pb-2">
|
||||
<div className="text-[10px] font-bold text-text-muted/60 tracking-widest px-3 mb-2">TOOLS</div>
|
||||
<div className="flex items-center px-3 py-1.5 rounded-md text-[13px] font-medium text-text-secondary hover:bg-item-hover hover:text-text-primary cursor-default transition-colors mb-[2px]">
|
||||
<CalendarClock className="w-4 h-4 mr-2.5 opacity-70" strokeWidth={2} /><span>Scheduler</span>
|
||||
<section>
|
||||
<div className="text-[11px] font-semibold text-text-muted px-2.5 mb-1.5">Tools</div>
|
||||
<div className="flex items-center px-2.5 py-1.5 rounded-lg text-[13px] text-text-primary hover:bg-item-hover cursor-default transition-colors mb-0.5">
|
||||
<CalendarClock className="w-4 h-4 mr-2 text-text-secondary" strokeWidth={2} /><span>Scheduler</span>
|
||||
</div>
|
||||
<div className="flex items-center px-3 py-1.5 rounded-md text-[13px] font-medium text-text-secondary hover:bg-item-hover hover:text-text-primary cursor-default transition-colors mb-[2px]">
|
||||
<Gauge className="w-4 h-4 mr-2.5 opacity-70" strokeWidth={2} /><span>Speed Limiter</span>
|
||||
<div className="flex items-center px-2.5 py-1.5 rounded-lg text-[13px] text-text-primary hover:bg-item-hover cursor-default transition-colors">
|
||||
<Gauge className="w-4 h-4 mr-2 text-text-secondary" strokeWidth={2} /><span>Speed Limiter</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 pt-4 mt-auto">
|
||||
<div
|
||||
<div className="shrink-0 border-t border-border-color bg-sidebar-glass px-2 py-2 backdrop-blur-xl">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => useSettingsStore.getState().setActiveView('settings')}
|
||||
className={`flex items-center px-3 py-2 rounded-md text-[13px] font-medium cursor-pointer transition-colors ${
|
||||
className={`flex w-full items-center px-2.5 py-2 rounded-lg text-[13px] text-left cursor-default transition-colors ${
|
||||
activeView === 'settings'
|
||||
? 'bg-[#3B66DE] text-white shadow-sm'
|
||||
: 'text-text-secondary hover:bg-[#2A2C2F] hover:text-text-primary'
|
||||
? 'bg-accent text-white font-medium'
|
||||
: 'text-text-primary hover:bg-item-hover'
|
||||
}`}
|
||||
>
|
||||
<Settings className={`w-4 h-4 mr-2.5 ${activeView === 'settings' ? 'opacity-100' : 'opacity-70'}`} strokeWidth={activeView === 'settings' ? 2.5 : 2} /><span>Settings</span>
|
||||
</div>
|
||||
<Settings className={`w-4 h-4 mr-2 ${activeView === 'settings' ? 'text-white' : 'text-text-secondary'}`} strokeWidth={activeView === 'settings' ? 2.25 : 2} />
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
|
||||
interface WindowDragRegionProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function WindowDragRegion({ className = '' }: WindowDragRegionProps) {
|
||||
return (
|
||||
<div
|
||||
className={`h-10 shrink-0 cursor-default ${className}`}
|
||||
data-tauri-drag-region
|
||||
onPointerDown={(event) => {
|
||||
if (event.button === 0) {
|
||||
void getCurrentWindow().startDragging();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+61
-37
@@ -1,15 +1,15 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
/* Default/fallback Light */
|
||||
/* Default/fallback Light, matching Theme.modernLight. */
|
||||
--main-bg: 0 0% 98%;
|
||||
--sidebar-bg: 0 0% 93%;
|
||||
--sidebar-glass: 0 0% 93% / 0.6;
|
||||
--sidebar-bg: 0 0% 94%;
|
||||
--sidebar-glass: 0 0% 94% / 0.78;
|
||||
--border-color: 0 0% 88%;
|
||||
--item-hover: 0 0% 0% / 0.05;
|
||||
--item-selected: 211 100% 50% / 0.15;
|
||||
|
||||
--accent-color: 211 100% 52%;
|
||||
|
||||
--text-primary: 0 0% 10%;
|
||||
--text-secondary: 0 0% 30%;
|
||||
--text-muted: 0 0% 50%;
|
||||
@@ -19,12 +19,14 @@
|
||||
}
|
||||
|
||||
.theme-light {
|
||||
color-scheme: light;
|
||||
--main-bg: 0 0% 98%;
|
||||
--sidebar-bg: 0 0% 93%;
|
||||
--sidebar-glass: 0 0% 93% / 0.6;
|
||||
--sidebar-bg: 0 0% 94%;
|
||||
--sidebar-glass: 0 0% 94% / 0.78;
|
||||
--border-color: 0 0% 88%;
|
||||
--item-hover: 0 0% 0% / 0.05;
|
||||
--item-selected: 211 100% 50% / 0.15;
|
||||
--accent-color: 211 100% 52%;
|
||||
--text-primary: 0 0% 10%;
|
||||
--text-secondary: 0 0% 30%;
|
||||
--text-muted: 0 0% 50%;
|
||||
@@ -34,58 +36,64 @@
|
||||
}
|
||||
|
||||
.theme-dark {
|
||||
/* Modern Mac Dark - Lighter Grays matching SwiftUI */
|
||||
color-scheme: dark;
|
||||
/* Native dark approximation for SwiftUI's system appearance. */
|
||||
--main-bg: 0 0% 16%;
|
||||
--sidebar-bg: 0 0% 14%;
|
||||
--sidebar-glass: 0 0% 14% / 0.6;
|
||||
--border-color: 0 0% 10%;
|
||||
--sidebar-glass: 0 0% 14% / 0.78;
|
||||
--border-color: 0 0% 24%;
|
||||
--item-hover: 0 0% 100% / 0.08;
|
||||
--item-selected: 211 100% 50% / 0.2;
|
||||
--accent-color: 211 100% 52%;
|
||||
--text-primary: 0 0% 98%;
|
||||
--text-secondary: 0 0% 75%;
|
||||
--text-muted: 0 0% 55%;
|
||||
--bg-modal: 0 0% 18%;
|
||||
--bg-input: 0 0% 0% / 0.3;
|
||||
--border-modal: 0 0% 10%;
|
||||
--border-modal: 0 0% 28%;
|
||||
}
|
||||
|
||||
.theme-dracula {
|
||||
/* Dracula Theme */
|
||||
--main-bg: 231 15% 18%;
|
||||
--sidebar-bg: 232 14% 14%;
|
||||
--sidebar-glass: 232 14% 14% / 0.6;
|
||||
--border-color: 232 14% 10%;
|
||||
color-scheme: dark;
|
||||
/* Exact values from Theme.dracula. */
|
||||
--main-bg: 240 14% 19%;
|
||||
--sidebar-bg: 233 13% 31%;
|
||||
--sidebar-glass: 233 13% 31% / 0.78;
|
||||
--border-color: 232 13% 38%;
|
||||
--item-hover: 0 0% 100% / 0.08;
|
||||
--item-selected: 265 89% 78% / 0.2;
|
||||
--text-primary: 60 30% 96%;
|
||||
--text-secondary: 225 27% 65%;
|
||||
--text-muted: 225 27% 55%;
|
||||
--bg-modal: 231 15% 20%;
|
||||
--bg-input: 232 14% 12%;
|
||||
--border-modal: 232 14% 10%;
|
||||
--item-selected: 340 100% 74% / 0.2;
|
||||
--accent-color: 340 100% 74%;
|
||||
--text-primary: 60 25% 96%;
|
||||
--text-secondary: 222 21% 48%;
|
||||
--text-muted: 222 17% 62%;
|
||||
--bg-modal: 240 14% 21%;
|
||||
--bg-input: 240 14% 15%;
|
||||
--border-modal: 233 13% 39%;
|
||||
}
|
||||
|
||||
.theme-nord {
|
||||
/* Nord Theme */
|
||||
color-scheme: dark;
|
||||
/* Exact values from Theme.nord. */
|
||||
--main-bg: 220 16% 22%;
|
||||
--sidebar-bg: 222 16% 19%;
|
||||
--sidebar-glass: 222 16% 19% / 0.6;
|
||||
--border-color: 222 16% 15%;
|
||||
--sidebar-bg: 221 16% 28%;
|
||||
--sidebar-glass: 221 16% 28% / 0.78;
|
||||
--border-color: 220 15% 35%;
|
||||
--item-hover: 0 0% 100% / 0.08;
|
||||
--item-selected: 193 43% 67% / 0.2;
|
||||
--text-primary: 218 27% 92%;
|
||||
--text-secondary: 214 20% 78%;
|
||||
--text-muted: 220 11% 54%;
|
||||
--accent-color: 194 44% 67%;
|
||||
--text-primary: 220 25% 88%;
|
||||
--text-secondary: 212 18% 64%;
|
||||
--text-muted: 212 14% 72%;
|
||||
--bg-modal: 220 16% 25%;
|
||||
--bg-input: 222 16% 15%;
|
||||
--border-modal: 222 16% 10%;
|
||||
--bg-input: 220 16% 19%;
|
||||
--border-modal: 220 15% 36%;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-sidebar-bg: hsl(var(--sidebar-bg));
|
||||
--color-sidebar-glass: hsl(var(--sidebar-glass));
|
||||
--color-main-bg: hsl(var(--main-bg));
|
||||
--color-accent: #0A84FF;
|
||||
--color-accent: hsl(var(--accent-color));
|
||||
--color-border-color: hsl(var(--border-color));
|
||||
--color-item-hover: hsl(var(--item-hover));
|
||||
--color-item-selected: hsl(var(--item-selected));
|
||||
@@ -97,7 +105,7 @@
|
||||
--color-bg-input: hsl(var(--bg-input));
|
||||
--color-border-modal: hsl(var(--border-modal));
|
||||
|
||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -124,9 +132,14 @@
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
html[data-font-size="standard"] { font-size: 14px; }
|
||||
html[data-font-size="large"] { font-size: 16px; }
|
||||
html[data-font-size="extra-large"] { font-size: 18px; }
|
||||
:focus-visible {
|
||||
outline: 2px solid hsl(var(--accent-color));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
html[data-font-size="small"] { font-size: 12px; }
|
||||
html[data-font-size="standard"] { font-size: 13px; }
|
||||
html[data-font-size="large"] { font-size: 15px; }
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
@@ -149,3 +162,14 @@
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.settings-tab-label {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.settings-tab-label {
|
||||
font-size: 9px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ const getSiteLogin = (url: string, settings: ReturnType<typeof useSettingsStore.
|
||||
const syncSystemIntegrations = () => {
|
||||
const settings = useSettingsStore.getState();
|
||||
const activeCount = useDownloadStore.getState().downloads.filter(d => d.status === 'downloading').length;
|
||||
invoke('update_dock_badge', { count: activeCount }).catch(() => {});
|
||||
invoke('update_dock_badge', { count: settings.showDockBadge ? activeCount : 0 }).catch(() => {});
|
||||
if (settings.preventsSleepWhileDownloading) {
|
||||
invoke('set_prevent_sleep', { prevent: activeCount > 0 }).catch(() => {});
|
||||
} else {
|
||||
|
||||
@@ -8,6 +8,10 @@ export interface SiteLogin {
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export type AppFontSize = 'small' | 'standard' | 'large';
|
||||
export type ListRowDensity = 'compact' | 'standard' | 'relaxed';
|
||||
export type SettingsTab = 'downloads' | 'lookandfeel' | 'network' | 'locations' | 'sitelogins' | 'power' | 'engine' | 'integrations' | 'about';
|
||||
|
||||
export interface SettingsState {
|
||||
theme: 'dark' | 'light' | 'system' | 'dracula' | 'nord';
|
||||
defaultDownloadPath: string;
|
||||
@@ -15,14 +19,17 @@ export interface SettingsState {
|
||||
globalSpeedLimit: string;
|
||||
isSidebarVisible: boolean;
|
||||
activeView: 'downloads' | 'settings';
|
||||
activeSettingsTab: SettingsTab;
|
||||
|
||||
// Replicated SwiftUI App Settings
|
||||
perServerConnections: number;
|
||||
maxAutomaticRetries: number;
|
||||
showNotifications: boolean;
|
||||
playCompletionSound: boolean;
|
||||
appFontSize: 'standard' | 'large' | 'extra-large';
|
||||
listRowDensity: 'compact' | 'standard' | 'spacious';
|
||||
appFontSize: AppFontSize;
|
||||
listRowDensity: ListRowDensity;
|
||||
showDockBadge: boolean;
|
||||
showMenuBarIcon: boolean;
|
||||
proxyMode: 'none' | 'system' | 'custom';
|
||||
proxyHost: string;
|
||||
proxyPort: number;
|
||||
@@ -39,14 +46,17 @@ export interface SettingsState {
|
||||
setMaxConcurrentDownloads: (count: number) => void;
|
||||
setGlobalSpeedLimit: (limit: string) => void;
|
||||
setActiveView: (view: 'downloads' | 'settings') => void;
|
||||
setActiveSettingsTab: (tab: SettingsTab) => void;
|
||||
toggleSidebar: () => void;
|
||||
|
||||
setPerServerConnections: (count: number) => void;
|
||||
setMaxAutomaticRetries: (count: number) => void;
|
||||
setShowNotifications: (show: boolean) => void;
|
||||
setPlayCompletionSound: (play: boolean) => void;
|
||||
setAppFontSize: (size: 'standard' | 'large' | 'extra-large') => void;
|
||||
setListRowDensity: (density: 'compact' | 'standard' | 'spacious') => void;
|
||||
setAppFontSize: (size: AppFontSize) => void;
|
||||
setListRowDensity: (density: ListRowDensity) => void;
|
||||
setShowDockBadge: (show: boolean) => void;
|
||||
setShowMenuBarIcon: (show: boolean) => void;
|
||||
setProxyMode: (mode: 'none' | 'system' | 'custom') => void;
|
||||
setProxyHost: (host: string) => void;
|
||||
setProxyPort: (port: number) => void;
|
||||
@@ -102,6 +112,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
maxConcurrentDownloads: 3,
|
||||
globalSpeedLimit: '',
|
||||
activeView: 'downloads',
|
||||
activeSettingsTab: 'downloads',
|
||||
isSidebarVisible: true,
|
||||
|
||||
// Replicated SwiftUI defaults
|
||||
@@ -111,6 +122,8 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
playCompletionSound: true,
|
||||
appFontSize: 'standard',
|
||||
listRowDensity: 'standard',
|
||||
showDockBadge: true,
|
||||
showMenuBarIcon: true,
|
||||
proxyMode: 'none',
|
||||
proxyHost: '',
|
||||
proxyPort: 8080,
|
||||
@@ -135,6 +148,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
setMaxConcurrentDownloads: (max) => set({ maxConcurrentDownloads: max }),
|
||||
setGlobalSpeedLimit: (limit) => set({ globalSpeedLimit: limit }),
|
||||
setActiveView: (view) => set({ activeView: view }),
|
||||
setActiveSettingsTab: (activeSettingsTab) => set({ activeSettingsTab }),
|
||||
toggleSidebar: () => set((state) => ({ isSidebarVisible: !state.isSidebarVisible })),
|
||||
|
||||
setPerServerConnections: (perServerConnections) => set({ perServerConnections }),
|
||||
@@ -143,6 +157,8 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
setPlayCompletionSound: (playCompletionSound) => set({ playCompletionSound }),
|
||||
setAppFontSize: (appFontSize) => set({ appFontSize }),
|
||||
setListRowDensity: (listRowDensity) => set({ listRowDensity }),
|
||||
setShowDockBadge: (showDockBadge) => set({ showDockBadge }),
|
||||
setShowMenuBarIcon: (showMenuBarIcon) => set({ showMenuBarIcon }),
|
||||
setProxyMode: (proxyMode) => set({ proxyMode }),
|
||||
setProxyHost: (proxyHost) => set({ proxyHost }),
|
||||
setProxyPort: (proxyPort) => set({ proxyPort }),
|
||||
@@ -170,6 +186,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
maxConcurrentDownloads: state.maxConcurrentDownloads,
|
||||
globalSpeedLimit: state.globalSpeedLimit,
|
||||
isSidebarVisible: state.isSidebarVisible,
|
||||
activeSettingsTab: state.activeSettingsTab,
|
||||
|
||||
perServerConnections: state.perServerConnections,
|
||||
maxAutomaticRetries: state.maxAutomaticRetries,
|
||||
@@ -177,6 +194,8 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
playCompletionSound: state.playCompletionSound,
|
||||
appFontSize: state.appFontSize,
|
||||
listRowDensity: state.listRowDensity,
|
||||
showDockBadge: state.showDockBadge,
|
||||
showMenuBarIcon: state.showMenuBarIcon,
|
||||
proxyMode: state.proxyMode,
|
||||
proxyHost: state.proxyHost,
|
||||
proxyPort: state.proxyPort,
|
||||
@@ -191,6 +210,8 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
merge: (persistedState: any, currentState) => ({
|
||||
...currentState,
|
||||
...persistedState,
|
||||
appFontSize: persistedState?.appFontSize === 'extra-large' ? 'large' : (persistedState?.appFontSize || currentState.appFontSize),
|
||||
listRowDensity: persistedState?.listRowDensity === 'spacious' ? 'relaxed' : (persistedState?.listRowDensity || currentState.listRowDensity),
|
||||
downloadDirectories: (persistedState && typeof persistedState === 'object' && persistedState.downloadDirectories)
|
||||
? persistedState.downloadDirectories
|
||||
: currentState.downloadDirectories,
|
||||
|
||||
Reference in New Issue
Block a user