import { useEffect, useLayoutEffect, useRef, type CSSProperties, type JSX } from 'react' import { useAudioStore } from '../stores/audioStore' import { useSettingsStore } from '../stores/settingsStore' import { useThemeStore, PRESETS, PRESET_IDS } from '../stores/themeStore' import type { ScopeKind } from '../../types/scope' import { SCOPE_KINDS } from '../../types/scope' const SCOPE_LABELS: Record = { spectrum: 'Spectrum', oscilloscope: 'Oscilloscope', vectorscope: 'Vectorscope', spectrogram: 'Spectrogram', vumeter: 'VU Meter', lufsmeter: 'LUFS Meter', waveform: 'Waveform', } interface BottomBarProps { onClose: () => void onHeightChange?: (height: number) => void } export default function BottomBar({ onClose, onHeightChange }: BottomBarProps): JSX.Element { const rootRef = useRef(null) const hiddenScopes = useSettingsStore((s) => s.hiddenScopes) const toggleScope = useSettingsStore((s) => s.toggleScope) const { presetId, accent, setPreset, setCustomAccent, customAccent } = useThemeStore() const { systemSources, devices, selectedSystemSourceId, selectedDeviceId, captureMode, isCapturing, captureStatus, captureError, inputGainDb, refreshSystemSources, refreshDevices, refreshBackendSupport, selectSystemSource, selectDevice, startCapture, setInputGain, } = useAudioStore() useEffect(() => { void refreshBackendSupport() void refreshSystemSources() void refreshDevices() }, [refreshBackendSupport, refreshSystemSources, refreshDevices]) useLayoutEffect(() => { if (!onHeightChange || !rootRef.current) return const rootElement = rootRef.current let frameId = 0 const reportHeight = (): void => { cancelAnimationFrame(frameId) frameId = requestAnimationFrame(() => { onHeightChange(Math.ceil(rootElement.getBoundingClientRect().height)) }) } reportHeight() const resizeObserver = new ResizeObserver(() => { reportHeight() }) resizeObserver.observe(rootElement) return () => { cancelAnimationFrame(frameId) resizeObserver.disconnect() } }, [onHeightChange]) const handleSourceChange = async (value: string): Promise => { if (value.startsWith('system:')) { const sourceId = value.slice('system:'.length) await selectSystemSource(sourceId) await startCapture() return } if (value.startsWith('device:')) { const deviceId = value.slice('device:'.length) await selectDevice(deviceId) await startCapture() } } const selectedSourceValue = captureMode === 'system' ? `system:${selectedSystemSourceId ?? systemSources[0]?.id ?? '__default_system_output__'}` : `device:${selectedDeviceId ?? ''}` const visibleSystemSources = systemSources.length ? systemSources : [{ id: '__default_system_output__', label: 'System Output', kind: 'system', isDefault: true }] const showInputDevices = devices.length > 0 const indicatorLabel = isCapturing ? 'Capturing' : captureStatus === 'connecting' ? 'Connecting' : captureStatus === 'error' ? 'Capture Failed' : 'Idle' const trimPercent = Math.min(100, Math.max(0, ((inputGainDb + 12) / 24) * 100)) return (
Modules
{SCOPE_KINDS.map((kind) => { const active = !hiddenScopes.has(kind) return ( ) })}
Theme
{PRESET_IDS.map((id) => { const preset = PRESETS[id] const active = presetId === id && !customAccent return ( )}
Audio Source
{indicatorLabel}
{captureError ? (
{captureError}
) : null}
Trim
{inputGainDb > 0 ? '+' : ''}{inputGainDb.toFixed(1)}dB setInputGain(Number(event.target.value))} onDoubleClick={() => setInputGain(0)} />
) }