import type { FormEvent, ReactNode } from 'react'; import { useState } from 'react'; import { Loader2, Plug, ShieldCheck } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Textarea } from '@/components/ui/textarea'; import { useMaintenance, useSession } from './useTransmissionData'; // The daemon's settings, as one form with a draft. // // Only the fields you actually changed are sent — session-set is a partial update, and posting the whole // object back would race the 5-second poll and re-apply stale values you never touched. The sidecar reads // the settings back after writing because Transmission clamps several of them, so the form snaps to what // the daemon really holds rather than to what you typed. export const SettingsView = () => { const { session, isLoading, save } = useSession(); const { portTest, updateBlocklist } = useMaintenance(); const [draft, setDraft] = useState>({}); if (isLoading && !session) { return (
Loading settings…
); } if (!session) { return (
Could not read the session settings.
); } const get = (key: string, fallback: T): T => key in draft ? (draft[key] as T) : ((session[key] as T) ?? fallback); const set = (key: string, value: unknown) => setDraft((prev) => ({ ...prev, [key]: value })); const dirty = Object.keys(draft).length > 0; const apply = async (ev?: FormEvent) => { ev?.preventDefault(); await save.mutateAsync(draft); setDraft({}); }; const bool = (key: string, label: string, hint?: string) => ( set(key, v)} label={label} hint={hint} /> ); const number = (key: string, label: string, unit?: string, min = 0) => ( set(key, Number(ev.target.value))} className="w-32" /> ); const text = (key: string, label: string, placeholder?: string) => (
set(key, ev.target.value)} className="font-mono text-xs" />
); return (
Downloads Bandwidth Network Queue Other
{text('download-dir', 'Download directory', '/downloads')} {bool('incomplete-dir-enabled', 'Keep incomplete files somewhere else')} {get('incomplete-dir-enabled', false) && text('incomplete-dir', 'Incomplete directory')} {bool( 'rename-partial-files', 'Append .part to incomplete files', 'Stops media scanners picking up half-downloaded files.', )}
{bool('start-added-torrents', 'Start torrents as soon as they are added')} {bool('trash-original-torrent-files', 'Delete the .torrent file after adding')}
{bool('seedRatioLimited', 'Stop seeding at a ratio')} {get('seedRatioLimited', false) && ( set('seedRatioLimit', Number(ev.target.value))} className="w-32" /> )} {bool('idle-seeding-limit-enabled', 'Stop seeding when idle')} {get('idle-seeding-limit-enabled', false) && number('idle-seeding-limit', 'Idle for', 'minutes')}
{bool('speed-limit-down-enabled', 'Limit download speed')} {get('speed-limit-down-enabled', false) && number('speed-limit-down', 'Download', 'kB/s')} {bool('speed-limit-up-enabled', 'Limit upload speed')} {get('speed-limit-up-enabled', false) && number('speed-limit-up', 'Upload', 'kB/s')}
{bool('alt-speed-enabled', 'Use the alternative limits now')} {number('alt-speed-down', 'Download', 'kB/s')} {number('alt-speed-up', 'Upload', 'kB/s')} {bool('alt-speed-time-enabled', 'Switch automatically on a schedule')} {get('alt-speed-time-enabled', false) && ( <> set('alt-speed-time-begin', v)} /> set('alt-speed-time-end', v)} /> set('alt-speed-time-day', v)} /> )}
{number('peer-port', 'Peer port', undefined, 1)} {bool('peer-port-random-on-start', 'Pick a random port on each start')} {bool('port-forwarding-enabled', 'Ask the router to forward it (UPnP / NAT-PMP)')}
{number('peer-limit-global', 'Maximum peers overall', undefined, 1)} {number('peer-limit-per-torrent', 'Maximum peers per torrent', undefined, 1)}
{bool('pex-enabled', 'Peer exchange (PEX)')} {bool('dht-enabled', 'Distributed hash table (DHT)')} {bool('lpd-enabled', 'Local peer discovery')} {bool('utp-enabled', 'µTP — yields to other traffic on your connection')}
{bool('blocklist-enabled', 'Use a blocklist')} {text('blocklist-url', 'Blocklist URL')}
{Number(session['blocklist-size'] ?? 0).toLocaleString()} rules loaded
{bool('download-queue-enabled', 'Limit how many torrents download at once')} {get('download-queue-enabled', false) && number('download-queue-size', 'At most', 'torrents', 1)}
{bool('seed-queue-enabled', 'Limit how many torrents seed at once')} {get('seed-queue-enabled', false) && number('seed-queue-size', 'At most', 'torrents', 1)}
{bool('queue-stalled-enabled', 'Treat torrents with no activity as stalled')} {get('queue-stalled-enabled', false) && number('queue-stalled-minutes', 'After', 'minutes', 1)}
{number('cache-size-mb', 'Disk cache', 'MB')}
{bool('script-torrent-done-enabled', 'Run a script when a torrent finishes')} {get('script-torrent-done-enabled', false) && text('script-torrent-done-filename', 'Script path', '/etc/transmission/done.sh')}