feat(desktop): align UI with SwiftUI

This commit is contained in:
NimBold
2026-06-12 20:59:49 +03:30
parent a5022fbf8a
commit 4c21a36083
8 changed files with 329 additions and 234 deletions
+7
View File
@@ -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 () => {
+48 -52
View File
@@ -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>
+106 -81
View File
@@ -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>
+62 -59
View File
@@ -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
View File
@@ -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;
}
}
+1 -1
View File
@@ -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 {
+25 -4
View File
@@ -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,