import { useState, useEffect, useCallback } from 'react'; import type { Config, AudioDevice, ToastItem } from './types'; import * as api from './api'; import { ToastContainer } from './components/Toast'; import { Connection } from './components/Connection'; import { GlobalLeds } from './components/GlobalLeds'; import { KnobCard } from './components/KnobCard'; import { ButtonCard } from './components/ButtonCard'; import { Presets } from './components/Presets'; const KNOB_INDICES = ['0', '1', '2', '3', '4'] as const; const BTN_INDICES = ['0', '1', '2', '3', '4'] as const; let toastSeq = 0; export default function App() { const [config, setConfig] = useState(null); const [saved, setSaved] = useState(null); const [presets, setPresets] = useState([]); const [toasts, setToasts] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [runningApps, setRunningApps] = useState([]); const [sinks, setSinks] = useState([]); const [sources, setSources] = useState([]); const [appsLoading, setAppsLoading] = useState(false); const addToast = useCallback((message: string, type: ToastItem['type'] = 'success') => { const id = ++toastSeq; setToasts((prev) => [...prev, { id, message, type }]); }, []); const removeToast = useCallback((id: number) => { setToasts((prev) => prev.filter((t) => t.id !== id)); }, []); const loadPresets = useCallback(async () => { try { const names = await api.listPresets(); setPresets(names); } catch { // Non-fatal — presets dir may not exist yet } }, []); const loadApps = useCallback(async () => { setAppsLoading(true); try { const [apps, sinkList, sourceList] = await Promise.all([ api.fetchRunningApps(), api.fetchSinks(), api.fetchSources(), ]); setRunningApps(apps); setSinks(sinkList); setSources(sourceList); } finally { setAppsLoading(false); } }, []); // Initial load useEffect(() => { void (async () => { try { const cfg = await api.fetchConfig(); setConfig(cfg); setSaved(cfg); } catch (err) { addToast(`Failed to load config: ${(err as Error).message}`, 'error'); } finally { setLoading(false); } await loadPresets(); await loadApps(); })(); }, [addToast, loadPresets, loadApps]); const isDirty = config !== null && JSON.stringify(config) !== JSON.stringify(saved); const handleSave = async () => { if (!config) return; setSaving(true); try { await api.saveConfig(config); setSaved(config); addToast('Config saved — daemon will reload automatically'); } catch (err) { addToast(`Save failed: ${(err as Error).message}`, 'error'); } finally { setSaving(false); } }; const handleRevert = () => { if (saved) setConfig(saved); }; const patchConfig = useCallback((key: K, value: Config[K]) => { setConfig((prev) => prev ? { ...prev, [key]: value } : prev); }, []); if (loading) { return (

Connecting to daemon…

); } if (!config) { return (

Could not reach the API server. Is turnup-ui running?

); } return ( <>

TurnUp

{isDirty && unsaved changes}
setConfig((prev) => prev ? { ...prev, ...patch } : prev)} /> patchConfig('leds', leds)} />

Knobs

{KNOB_INDICES.map((i) => ( patchConfig('knobs', { ...config.knobs, [i]: knob }) } /> ))}

Buttons

{BTN_INDICES.map((i) => ( patchConfig('buttons', { ...config.buttons, [i]: btn }) } /> ))}
setConfig(cfg)} onRefresh={loadPresets} onToast={addToast} />
); }