import { useCallback, useRef, useState, useEffect } from 'react'; import { type AppFontSize, type ListRowDensity, type SettingsState, SettingsTab, useSettingsStore } from '../store/useSettingsStore'; import { Download, Palette, Globe, Folder, Key, Moon, Terminal, Puzzle, Info, Plus, Trash2, Copy, RefreshCw, Code, ShieldAlert } from 'lucide-react'; import { open } from '@tauri-apps/plugin-dialog'; import { getVersion } from '@tauri-apps/api/app'; import { openUrl } from '@tauri-apps/plugin-opener'; import { invokeCommand as invoke } from '../ipc'; import { useToast, ToastVariant } from '../contexts/ToastContext'; import type { EngineStatusItem } from '../bindings/EngineStatusItem'; import { WindowDragRegion } from './WindowDragRegion'; import appIcon from '../assets/app-icon.png'; import { DEFAULT_CATEGORY_SUBFOLDERS, DOWNLOAD_CATEGORIES, normalizeCategorySubfolder } from '../utils/downloadLocations'; import { usePlatformInfo } from '../utils/platform'; 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 }, ]; const engineChecks = [ { kind: 'aria2', name: 'Aria2', command: 'get_aria2_engine_status' }, { kind: 'ytdlp', name: 'yt-dlp', command: 'get_ytdlp_engine_status' }, { kind: 'ffmpeg', name: 'FFmpeg', command: 'get_ffmpeg_engine_status' }, { kind: 'deno', name: 'Deno', command: 'get_deno_engine_status' }, ] as const; type EngineCheck = typeof engineChecks[number]; const engineStatusCache = new Map(); const engineStatusInFlight = new Map>(); const upsertEngineStatus = (items: EngineStatusItem[], item: EngineStatusItem) => { const next = items.filter(existing => existing.kind !== item.kind); next.push(item); return next; }; const buildEngineStatusError = (check: EngineCheck, error: unknown): EngineStatusItem => ({ name: check.name, kind: check.kind, expected_sidecar: '', resolved_path: null, version: null, ready: false, error: String(error), stderr_tail: null, remediation_hint: null, rpc_port: null, daemon_alive: null, rpc_ready: null, last_stderr_tail: null, expects_internal_dir: null, has_internal_dir: null, has_python_framework: null, }); const runEngineStatusCheck = (check: EngineCheck, force: boolean) => { if (!force) { const cached = engineStatusCache.get(check.kind); if (cached) return Promise.resolve(cached); } if (!force) { const inFlight = engineStatusInFlight.get(check.kind); if (inFlight) return inFlight; } if (force) engineStatusCache.delete(check.kind); const promise = invoke(check.command) .then(item => { if (item.ready) engineStatusCache.set(item.kind, item); return item; }) .catch(error => buildEngineStatusError(check, error)) .finally(() => { if (engineStatusInFlight.get(check.kind) === promise) { engineStatusInFlight.delete(check.kind); } }); engineStatusInFlight.set(check.kind, promise); return promise; }; const CategoryFolderInput = ({ category, settings, onBrowse }: { category: string; settings: SettingsState; onBrowse: () => void; }) => { const base = settings.baseDownloadFolder.replace(/\/+$/, '') || '~/Downloads'; const sub = settings.categorySubfolders[category] || DEFAULT_CATEGORY_SUBFOLDERS[category as keyof typeof DEFAULT_CATEGORY_SUBFOLDERS]; const override = settings.categoryDirectoryOverrides[category]; const displayPath = override ?? `${base}/${sub}`; const [localValue, setLocalValue] = useState(null); const value = localValue !== null ? localValue : displayPath; return (
setLocalValue(displayPath)} onChange={(e) => setLocalValue(e.target.value)} onBlur={() => { const val = localValue ?? displayPath; const basePrefix = base + '/'; if (!val.trim()) { settings.setCategoryDirectoryOverride(category, undefined); settings.setCategorySubfolder(category, ''); } else if (val.startsWith(basePrefix)) { settings.setCategoryDirectoryOverride(category, undefined); settings.setCategorySubfolder( category, normalizeCategorySubfolder( val.substring(basePrefix.length), DEFAULT_CATEGORY_SUBFOLDERS[category as keyof typeof DEFAULT_CATEGORY_SUBFOLDERS] ) ); } else { settings.setCategoryDirectoryOverride(category, val.trim()); } setLocalValue(null); }} className="app-control flex-1 max-w-[280px] text-[12px] px-3 py-1.5 bg-surface-overlay/50 border-border-color/50 focus:border-accent-color focus:bg-surface-overlay" aria-label={`${category} subfolder`} /> {override && ( )}
); }; export default function SettingsView() { const settings = useSettingsStore(); const activeTab = settings.activeSettingsTab; const platform = usePlatformInfo(); // Local state for engine status const [engineStatus, setEngineStatus] = useState(null); const [expandedEngine, setExpandedEngine] = useState(null); const [isRecheckingEngines, setIsRecheckingEngines] = useState(false); const engineRunId = useRef(0); const [appVersion, setAppVersion] = useState('0.7.3'); const [extensionServerPort, setExtensionServerPort] = useState(null); // Local state for adding site login const [loginPattern, setLoginPattern] = useState(''); const [loginUser, setLoginUser] = useState(''); const [loginPass, setLoginPass] = useState(''); const [loginError, setLoginError] = useState(''); // Toast notifications const { addToast } = useToast(); const [isCheckingForUpdates, setIsCheckingForUpdates] = useState(false); useEffect(() => { getVersion().then(setAppVersion).catch(() => undefined); }, []); useEffect(() => { if (settings.activeView !== 'settings' || activeTab !== 'integrations') return; let active = true; const refresh = () => { invoke('get_extension_server_port') .then(port => { if (active) setExtensionServerPort(port); }) .catch(() => { if (active) setExtensionServerPort(null); }); }; refresh(); const timer = window.setInterval(refresh, 3000); return () => { active = false; window.clearInterval(timer); }; }, [settings.activeView, activeTab]); const runEngineChecks = useCallback((force = false) => { const runId = ++engineRunId.current; const cached = engineChecks .map(check => engineStatusCache.get(check.kind)) .filter((item): item is EngineStatusItem => Boolean(item)); setEngineStatus(force ? [] : cached); setExpandedEngine(null); const checksToRun = force ? engineChecks : engineChecks.filter(check => !engineStatusCache.has(check.kind)); if (checksToRun.length === 0) { setIsRecheckingEngines(false); return; } setIsRecheckingEngines(true); let remaining = checksToRun.length; checksToRun.forEach(check => { runEngineStatusCheck(check, force) .then(item => { if (engineRunId.current === runId) { setEngineStatus(current => upsertEngineStatus(current ?? [], item)); } }) .finally(() => { remaining -= 1; if (remaining === 0 && engineRunId.current === runId) { setIsRecheckingEngines(false); } }); }); }, []); // Fetch engine status when Engine tab is opened useEffect(() => { if (settings.activeView === 'settings' && activeTab === 'engine') { runEngineChecks(false); } }, [settings.activeView, activeTab, runEngineChecks]); const showToast = (msg: string, variant: ToastVariant = 'info') => { addToast({ message: msg, variant }); }; const findEngine = (kind: string) => engineStatus?.find(e => e.kind === kind) ?? null; const renderEngineStatus = (item: EngineStatusItem | null) => { if (!item) return Checking...; if (item.ready) return Ready; return Error / Missing; }; const renderEngineVersion = (item: EngineStatusItem | null) => { if (!item) return 'Checking...'; if (item.version) return item.version; if (item.error) return `Error: ${item.error.length > 80 ? item.error.substring(0, 80) + '…' : item.error}`; return 'Unknown'; }; const renderEngineDetails = (item: EngineStatusItem | null) => { if (!item || item.ready || (!item.error && !item.remediation_hint && !item.stderr_tail)) return null; const isExpanded = expandedEngine === item.kind; return ( <> {isExpanded && (
{item.resolved_path &&

Binary: {item.resolved_path}

} {item.expected_sidecar &&

Expected: {item.expected_sidecar}

} {item.error &&

Error: {item.error}

} {item.remediation_hint &&

Tip: {item.remediation_hint}

} {item.stderr_tail &&
stderr
{item.stderr_tail}
} {item.daemon_alive != null &&

Daemon process alive: {String(item.daemon_alive)}

} {item.rpc_ready != null &&

RPC ready: {String(item.rpc_ready)}

} {item.rpc_port != null &&

RPC port: {item.rpc_port}

} {item.last_stderr_tail &&
daemon stderr
{item.last_stderr_tail}
} {item.expects_internal_dir === true &&

Packaging: PyInstaller onedir (_internal required)

} {item.has_internal_dir === true &&

_internal directory found: true

} {item.has_python_framework != null &&

Python runtime found: {String(item.has_python_framework)}

}
)} ); }; const handleCheckForUpdates = async () => { if (isCheckingForUpdates) return; setIsCheckingForUpdates(true); showToast('Checking for updates...'); try { const result = await invoke('check_for_updates'); if (result.type === 'UpToDate') { showToast(`Firelink ${result.latest_version} is up to date`, 'success'); } else if (result.type === 'UpdateAvailable') { addToast({ variant: 'info', isActionable: true, message: (
Firelink {result.update.version} is available.
) }); } else { showToast('The update check returned an unexpected response', 'warning'); } } catch (error) { showToast(`Update check failed: ${String(error)}`, 'error'); } finally { setIsCheckingForUpdates(false); } }; const handleBrowseCategory = async (category: string) => { const currentPath = settings.categoryDirectoryOverrides[category] || settings.baseDownloadFolder; try { const selected = await open({ directory: true, multiple: false, defaultPath: currentPath.startsWith('~') ? undefined : currentPath }); if (selected && typeof selected === 'string') { const approvedPath = await settings.approveDownloadRoot(selected); settings.setCategoryDirectoryOverride(category, approvedPath); } } catch (e) { console.error(`Failed to select folder for ${category}:`, e); } }; const handleBrowseBase = async () => { try { const base = await open({ directory: true, multiple: false, defaultPath: settings.baseDownloadFolder.startsWith('~') ? undefined : settings.baseDownloadFolder }); if (base && typeof base === 'string') { const approvedBase = await settings.approveDownloadRoot(base); settings.setBaseDownloadFolder(approvedBase); try { const safeSubfolders = Object.fromEntries( DOWNLOAD_CATEGORIES.map(category => [ category, normalizeCategorySubfolder( settings.categorySubfolders[category] || '', DEFAULT_CATEGORY_SUBFOLDERS[category] ) ]) ); await invoke('create_category_directories', { baseFolder: approvedBase, subfolders: safeSubfolders }); } catch (e) { console.error("Failed to create directories on disk:", e); showToast(`Base folder saved, but category folders could not be created: ${String(e)}`, 'warning'); return; } showToast("Base download folder updated", 'success'); } } catch (e) { console.error("Failed to browse base path:", e); } }; const handleAddLogin = async () => { if (!loginPattern.trim() || !loginUser.trim()) { setLoginError("Please enter a URL pattern and a username."); return; } const id = crypto.randomUUID(); if (loginPass) { try { await invoke('set_keychain_password', { id, password: loginPass }); } catch (e) { console.error("Failed to save password to keychain:", e); setLoginError("Failed to save password securely."); return; } } settings.addSiteLogin({ id, urlPattern: loginPattern.trim(), username: loginUser.trim() }); setLoginPattern(''); setLoginUser(''); setLoginPass(''); setLoginError(''); showToast("Added site credential", 'success'); }; const copyToken = async () => { try { await navigator.clipboard.writeText(settings.extensionPairingToken); showToast("Token copied to clipboard!", 'success'); } catch (error) { showToast(`Could not copy token: ${String(error)}`, 'error'); } }; 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 ( ); }; return (
{/* SwiftUI SettingsPaneContainer-style horizontal tab strip */}
{settingsTabs.map(tab => ( ))}
{/* Content Area */}

{activeTabLabel}

{/* Downloads Pane */} {activeTab === 'downloads' && (
Default connections: For new downloads
settings.setPerServerConnections(Number(e.target.value))} onBlur={(e) => { const val = Number(e.target.value); if (val < 1) settings.setPerServerConnections(1); if (val > 16) settings.setPerServerConnections(16); }} className="app-control w-24 text-center" />
Parallel downloads: Max simultaneous active files
settings.setMaxConcurrentDownloads(Number(e.target.value))} onBlur={(e) => { const val = Number(e.target.value); if (val < 1) settings.setMaxConcurrentDownloads(1); if (val > 12) settings.setMaxConcurrentDownloads(12); }} className="app-control w-24 text-center" />
Global speed limit: {settings.globalSpeedLimit || 'Unlimited'}
Automatic retries: If a connection fails
settings.setMaxAutomaticRetries(Number(e.target.value))} onBlur={(e) => { const val = Number(e.target.value); if (val < 0) settings.setMaxAutomaticRetries(0); if (val > 10) settings.setMaxAutomaticRetries(10); }} className="app-control w-24 text-center" />
)} {/* Look & Feel Pane */} {activeTab === 'lookandfeel' && (

App Theme

Theme
{[ { value: 'system', label: 'System', colors: ['#f4f4f5', '#252525'] }, { value: 'light', label: 'Light', colors: ['#ffffff', '#e9e9ec'] }, { value: 'dark', label: 'Dark', colors: ['#1a1a1a', '#292929'] }, { value: 'dracula', label: 'Dracula', colors: ['#282a36', '#ff79c6'] }, { value: 'nord', label: 'Nord', colors: ['#2e3440', '#88c0d0'] }, ].map(({ value, label, colors }) => ( ))}

Select a color palette for the app's user interface.

Display

Font Size
List Row Density

{platform.os === 'macos' ? 'macOS Integration' : 'Desktop Integration'}

{platform.os === 'macos' && ( )}
)} {/* Network Pane */} {activeTab === 'network' && (

Proxy

Mode
{[ ['none', 'No Proxy'], ['system', 'Use System Proxy'], ['custom', 'Custom Proxy'], ].map(([value, label]) => ( ))}
{settings.proxyMode === 'custom' && ( <>
Proxy Host settings.setProxyHost(e.target.value)} placeholder="127.0.0.1" className="app-control w-40 font-mono" />
Proxy Port settings.setProxyPort(Number(e.target.value))} onBlur={(e) => { const val = Number(e.target.value); if (val < 1) settings.setProxyPort(1); if (val > 65535) settings.setProxyPort(65535); }} className="app-control w-24 text-center" />
)}

{settings.proxyMode === 'none' && 'Downloads ignore configured proxies.'} {settings.proxyMode === 'system' && `Downloads use the detected ${platform.os === 'macos' ? 'macOS' : platform.os === 'windows' ? 'Windows' : 'desktop'} system proxy when available.`} {settings.proxyMode === 'custom' && (settings.proxyHost ? `Downloads use http://${settings.proxyHost}:${settings.proxyPort}.` : 'Enter a proxy host and port to enable the custom proxy.')}

Identity

Custom User Agent
settings.setCustomUserAgent(e.target.value)} placeholder="e.g. Mozilla/5.0..." className="app-control w-full font-mono text-[11px]" />

Spoofs the browser User-Agent to bypass download restrictions. Leave blank for default.

)} {/* Locations Pane */} {activeTab === 'locations' && (
Base Download Folder

Automatic category folders are created inside this folder.

settings.setBaseDownloadFolder(e.target.value)} className="app-control w-64 text-[11px] px-2" placeholder="~/Downloads" />
Category Subfolders Relative to the base folder
{DOWNLOAD_CATEGORIES.map((category) => (
{category} handleBrowseCategory(category)} />
))}
)} {/* Site Logins Pane */} {activeTab === 'sitelogins' && (

Site Credentials

{/* Site Logins List */}
{(settings.siteLogins || []).length === 0 ? (

No saved logins.

) : ( (settings.siteLogins || []).map((login) => (

{login.urlPattern}

User: {login.username}

)) )}
{/* Add Site Login Form */}

Add Site Credentials

{loginError && (

{loginError}

)}
setLoginPattern(e.target.value)} placeholder="e.g. *.example.com or example.com/downloads" className="bg-bg-input border border-border-modal rounded-md px-3 py-1.5 w-full text-text-primary focus:outline-none" />
setLoginUser(e.target.value)} placeholder="Username" className="bg-bg-input border border-border-modal rounded-md px-3 py-1.5 w-full text-text-primary focus:outline-none" />
setLoginPass(e.target.value)} placeholder="Password" className="bg-bg-input border border-border-modal rounded-md px-3 py-1.5 w-full text-text-primary focus:outline-none" />
)} {/* Power Pane */} {activeTab === 'power' && (

Power Management

)} {/* Engine Pane */} {activeTab === 'engine' && (

Media Downloader & Engines

Successful results are reused for this app session. Recheck runs real validation again.

{(() => { const a2 = findEngine('aria2'); const yt = findEngine('ytdlp'); const ff = findEngine('ffmpeg'); const dn = findEngine('deno'); return (
{/* aria2 card */}

Core Downloader (Aria2)

Version: {renderEngineVersion(a2)}
Status: {renderEngineStatus(a2)}
{renderEngineDetails(a2)}
{/* yt-dlp / ffmpeg / deno card */}

Media Extractors

{[ { key: 'ytdlp', item: yt, label: 'yt-dlp' }, { key: 'ffmpeg', item: ff, label: 'FFmpeg' }, { key: 'deno', item: dn, label: 'Deno' }, ].map(({ key, item, label }) => (
{label}: {renderEngineVersion(item)} {renderEngineStatus(item)}
{renderEngineDetails(item)}
))}

yt-dlp reads browser cookies to bypass video download limits or access restricted media. Firelink does not save browser cookies.

); })()}
)} {/* Integrations Pane */} {activeTab === 'integrations' && (

Connect Browser Extension

Capture downloads directly from your browser in three easy steps.

{!settings.isPairingTokenPersistent && (

Keychain Access Needed

Firelink needs macOS Keychain access to securely save your pairing token across app restarts. Currently, your extension will only stay connected for this session.

)} {/* Step Guide Cards */}
{/* Step 1 */}
1

Copy Token

This secure token authorizes your browser extension.

{/* Step 2 */}
2

Get Extension

Install the Firelink Companion extension on your browser.

{/* Step 3 */}
3

Paste & Connect

Click the Firelink icon in your browser's toolbar and paste the copied token.

{/* Status Info */}
Extension Server Status: {extensionServerPort ? `● Listening on 127.0.0.1:${extensionServerPort}` : '● Server unavailable'}
)} {/* About Pane */} {activeTab === 'about' && (
{/* Header Box */}
Firelink Icon

Firelink

Version {appVersion}

A fast desktop download manager powered by Rust and Tauri.

{/* Updates Section */}

Updates

Check for Updates

Firelink checks GitHub Releases for new versions.

{/* Credits Footer */}
Created by NimBold Source Code
Built with RustTauriReactTypeScript MIT License
Download engines: aria2yt-dlpFFmpegDeno
Copyright © 2026 NimBold. All rights reserved.
)}
); };