import { useLayoutEffect, useRef, useState, type CSSProperties, type JSX, type WheelEvent } from 'react' import { useNowPlayingStore } from '../stores/nowPlayingStore' import { useAudioStore } from '../stores/audioStore' import { usePerformanceStore } from '../stores/performanceStore' import { useSettingsStore } from '../stores/settingsStore' import { useThemeStore } from '../stores/themeStore' import { useUiStore } from '../stores/uiStore' import { useWindowBackgroundStore } from '../stores/windowBackgroundStore' import { useDesktopIntegrationStore } from '../stores/desktopIntegrationStore' import { getRendererWindowCapabilities } from '../windowCapabilities' import { getHorizontalWheelScrollResult } from '../utils/horizontalWheelScroll' import type { ScopeKind } from '../../types/scope' import { VISUALIZER_FRAME_TARGETS, type VisualizerFrameTarget } from '../../types/performance' import { SCOPE_KINDS } from '../../types/scope' import type { WindowBackgroundMode, WindowBackgroundState } from '../../types/windowState' import ThemedSelect from './ThemedSelect' const SCOPE_LABELS: Record = { spectrum: 'Spectrum', oscilloscope: 'Oscilloscope', vectorscope: 'Vectorscope', spectrogram: 'Spectrogram', vumeter: 'VU Meter', lufsmeter: 'Loudness Meter', waveform: 'Waveform', nowPlaying: 'Now Playing', } interface BottomBarProps { onClose: () => void onHeightChange?: (height: number) => void } const FRAME_TARGET_LABELS: Record = { 10: '10', 30: '30', 60: '60', 120: '120', 144: '144', 'display-sync': 'Sync', } const DEFAULT_INPUT_DEVICE_ID = '__default_input__' const WINDOW_BACKGROUND_MODES: readonly WindowBackgroundMode[] = ['solid', 'blurred', 'clear'] const WINDOW_BACKGROUND_MODE_LABELS: Record = { solid: 'Solid', blurred: 'Blurred', clear: 'Clear', } const WINDOW_BACKGROUND_MODE_TITLES: Record = { solid: 'Opaque themed background', blurred: 'Desktop shows through, blurred', clear: 'Desktop shows through, crisp', } const WINDOW_BACKGROUND_SET_THROTTLE_MS = 60 function getErrorMessage(error: unknown, fallback: string): string { return error instanceof Error && error.message ? error.message : fallback } type ThemeCreditSource = { credit?: string website?: string description?: string } | null | undefined type ThemeOptionLabelSource = { name: string credit?: string } export function resolveThemeOptionLabel(theme: ThemeOptionLabelSource): string { const credit = typeof theme.credit === 'string' && theme.credit.trim() ? theme.credit.trim() : null return credit ? `${theme.name} - ${credit}` : theme.name } export function resolveThemeCreditDetails(theme: ThemeCreditSource): { credit: string | null url: string | null description: string | null } { const credit = typeof theme?.credit === 'string' && theme.credit.trim() ? theme.credit.trim() : null if (!credit) { return { credit: null, url: null, description: null } } const website = typeof theme?.website === 'string' && theme.website.trim() ? theme.website.trim() : null const description = typeof theme?.description === 'string' && theme.description.trim() ? theme.description.trim() : null if (!website) { return { credit, url: null, description } } try { const parsed = new URL(website) if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { return { credit, url: parsed.toString(), description } } } catch { // Invalid URLs fall back to plain credit text. } return { credit, url: null, description } } export default function BottomBar({ onClose, onHeightChange }: BottomBarProps): JSX.Element { const rootRef = useRef(null) const [isRefreshingThemes, setIsRefreshingThemes] = useState(false) const windowBackground = useWindowBackgroundStore((s) => s.stored) const previewWindowBackground = useWindowBackgroundStore((s) => s.previewBackground) const setWindowBackground = useWindowBackgroundStore((s) => s.setBackground) const supportsBlurredBackground = getRendererWindowCapabilities().supportsBlurredBackground const pendingWindowBackgroundRef = useRef(null) const windowBackgroundFlushTimerRef = useRef | null>(null) const queueWindowBackgroundSave = (next: WindowBackgroundState): void => { previewWindowBackground(next) pendingWindowBackgroundRef.current = next if (windowBackgroundFlushTimerRef.current) return windowBackgroundFlushTimerRef.current = setTimeout(() => { windowBackgroundFlushTimerRef.current = null const pending = pendingWindowBackgroundRef.current pendingWindowBackgroundRef.current = null if (pending) { void setWindowBackground(pending) } }, WINDOW_BACKGROUND_SET_THROTTLE_MS) } const handleWindowBackgroundMode = (mode: WindowBackgroundMode): void => { if (mode === windowBackground.mode) return void setWindowBackground({ ...windowBackground, mode }) } const hiddenScopes = useSettingsStore((s) => s.hiddenScopes) const scopeOrder = useSettingsStore((s) => s.scopeOrder) const toggleScope = useSettingsStore((s) => s.toggleScope) const frameTarget = usePerformanceStore((s) => s.frameTarget) const dockedRenderFps = usePerformanceStore((s) => s.dockedRenderFps) const setFrameTarget = usePerformanceStore((s) => s.setFrameTarget) const { themes, activeTheme, activeThemeId, loadTheme, reloadThemes, showThemesFolder, } = useThemeStore() const nowPlayingState = useNowPlayingStore((s) => s.nowPlayingState) const retryNowPlayingProvider = useNowPlayingStore((s) => s.retryProvider) const openNowPlayingConfigWindow = useNowPlayingStore((s) => s.openConfigWindow) const { systemSources, devices, selectedSystemSourceId, selectedDeviceId, captureMode, isCapturing, captureStatus, captureError, captureNotice, inputGainDb, clearCaptureNotice, selectSystemSource, selectDevice, startCapture, setInputGain, } = useAudioStore() const showBanner = useUiStore((s) => s.showBanner) const desktopIntegration = useDesktopIntegrationStore((s) => s.snapshot) const desktopIntegrationBusy = useDesktopIntegrationStore((s) => s.busy) const desktopIntegrationError = useDesktopIntegrationStore((s) => s.error) const setCloseToTray = useDesktopIntegrationStore((s) => s.setCloseToTray) const setOpenAtLogin = useDesktopIntegrationStore((s) => s.setOpenAtLogin) const setLoginLaunchMode = useDesktopIntegrationStore((s) => s.setLoginLaunchMode) 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 === DEFAULT_INPUT_DEVICE_ID ? null : deviceId) await startCapture() } } const visibleSystemSources = systemSources.length ? systemSources : [{ id: '__default_system_output__', label: 'Default Output', kind: 'system', isDefault: true }] const defaultSystemSourceId = visibleSystemSources[0]?.id ?? '__default_system_output__' const selectedSourceValue = captureMode === 'system' ? `system:${selectedSystemSourceId ?? defaultSystemSourceId}` : `device:${selectedDeviceId ?? DEFAULT_INPUT_DEVICE_ID}` const indicatorLabel = isCapturing ? 'Capturing' : captureStatus === 'connecting' ? 'Connecting' : captureStatus === 'error' ? 'Capture Failed' : 'Idle' const trimPercent = Math.min(100, Math.max(0, ((inputGainDb + 12) / 24) * 100)) const roundedDockedRenderFps = Math.max(0, Math.round(dockedRenderFps)) const themeEntries = Object.entries(themes) const themeCredit = resolveThemeCreditDetails(activeTheme) const handleThemeChange = async (value: string): Promise => { await loadTheme(value) } const handleRetryCapture = async (): Promise => { clearCaptureNotice() await startCapture() } const handleUseDefaultSource = async (): Promise => { clearCaptureNotice() if (captureMode === 'system') { await selectSystemSource(defaultSystemSourceId) } else { await selectDevice(null) } await startCapture() } const handleRetryNowPlaying = async (): Promise => { const providerId = nowPlayingState.activeProviderId ?? nowPlayingState.providerPriority.find((candidate) => { const provider = nowPlayingState.providers[candidate] return provider.available && provider.isConfigured }) if (!providerId) { await openNowPlayingConfigWindow() return } try { await retryNowPlayingProvider(providerId) } catch (error) { showBanner({ tone: 'error', message: getErrorMessage(error, 'Could not reconnect the provider.'), actions: [], }) } } const handleShowThemesFolder = async (): Promise => { try { await showThemesFolder() } catch (error) { showBanner({ tone: 'error', message: getErrorMessage(error, 'Could not open the themes folder.'), actions: [], }) } } const handleReloadThemes = async (): Promise => { if (isRefreshingThemes) { return } setIsRefreshingThemes(true) try { await reloadThemes() } catch (error) { showBanner({ tone: 'error', message: getErrorMessage(error, 'Could not refresh themes.'), actions: [], }) } finally { setIsRefreshingThemes(false) } } const handleOpenThemeWebsite = async (url: string): Promise => { try { await window.electronAPI.openExternalUrl(url) } catch (error) { showBanner({ tone: 'error', message: getErrorMessage(error, 'Could not open the theme website.'), actions: [], }) } } const handleRailWheel = (event: WheelEvent): void => { const railElement = event.currentTarget const target = event.target const isTargetExcluded = target instanceof Element && target.closest('input[type="range"], select, .settings-control__select') !== null const scrollResult = getHorizontalWheelScrollResult({ clientWidth: railElement.clientWidth, deltaMode: event.deltaMode, deltaX: event.deltaX, deltaY: event.deltaY, isTargetExcluded, scrollLeft: railElement.scrollLeft, scrollWidth: railElement.scrollWidth, }) if (!scrollResult) return railElement.scrollLeft = scrollResult.nextScrollLeft event.preventDefault() } const currentNowPlayingProviderId = nowPlayingState.activeProviderId ?? nowPlayingState.providerPriority.find((providerId) => { const provider = nowPlayingState.providers[providerId] return provider.available && provider.isConfigured }) ?? null const currentNowPlayingProvider = currentNowPlayingProviderId ? nowPlayingState.providers[currentNowPlayingProviderId] : null const nowPlayingStatusLabel = nowPlayingState.onboardingRequired ? 'Not Set Up' : currentNowPlayingProvider?.connectionState === 'connected' ? 'Connected' : currentNowPlayingProvider?.connectionState === 'connecting' ? 'Connecting' : currentNowPlayingProvider?.connectionState === 'error' ? 'Error' : 'Idle' const nowPlayingSummary = nowPlayingState.onboardingRequired ? 'Set up Astra to enable live track data.' : `Priority: ${nowPlayingState.providerPriority .map((providerId) => nowPlayingState.definitions[providerId].label) .join(' → ')}` const captureMessage = captureError ?? captureNotice const nowPlayingErrorMessage = currentNowPlayingProvider?.lastError ?? currentNowPlayingProvider?.lastControlError ?? null const nowPlayingDetail = nowPlayingErrorMessage ? `${currentNowPlayingProviderId ? `${nowPlayingState.definitions[currentNowPlayingProviderId].label} · ` : ''}${nowPlayingErrorMessage}` : currentNowPlayingProviderId ? `${nowPlayingState.definitions[currentNowPlayingProviderId].label} · ${nowPlayingSummary}` : nowPlayingSummary const canUseDefaultSource = captureMode === 'system' ? selectedSystemSourceId !== defaultSystemSourceId : selectedDeviceId !== null const loginItemStatusMessage = desktopIntegration.loginItemStatus === 'requires-approval' ? 'Approval required in system login settings' : desktopIntegration.loginItemStatus === 'blocked' ? 'Disabled in system startup settings' : desktopIntegration.loginItemStatus === 'unavailable' ? 'Open at login is available in packaged builds' : desktopIntegrationError return (
Modules
{SCOPE_KINDS.map((kind) => { const active = scopeOrder.includes(kind) && !hiddenScopes.has(kind) return ( ) })}
Theme
{themeCredit.credit ? ( {themeCredit.url ? ( { event.preventDefault() void handleOpenThemeWebsite(themeCredit.url!) }} > By {themeCredit.credit} ) : ( By {themeCredit.credit} )} {themeCredit.description ? ( {themeCredit.description} ) : null} ) : null}
{ void handleThemeChange(event.target.value) }} className="bottom-bar__select" > {themeEntries.map(([id, theme]) => ( ))}
Window
{windowBackground.mode !== 'solid' || loginItemStatusMessage ? ( {windowBackground.mode !== 'solid' ? ( Window snapping is disabled in this mode ) : null} {windowBackground.mode !== 'solid' && loginItemStatusMessage ? ( ) : null} {loginItemStatusMessage ? ( {loginItemStatusMessage} ) : null} ) : null}
{WINDOW_BACKGROUND_MODES.map((mode) => { const unsupported = mode === 'blurred' && !supportsBlurredBackground return ( ) })}
{windowBackground.transparency}% { queueWindowBackgroundSave({ ...windowBackground, transparency: Number(event.target.value), }) }} title="How much of the desktop shows through" />
Now Playing
{nowPlayingDetail}
{nowPlayingStatusLabel}
{nowPlayingErrorMessage ? ( ) : null}
Audio Source
{ void handleSourceChange(event.target.value) }} className="bottom-bar__select" > {visibleSystemSources.map((source) => ( ))} {devices.map((device) => ( ))}
{indicatorLabel}
{captureMessage ? ( <>
{captureMessage}
{canUseDefaultSource ? ( ) : null} {!captureError && captureNotice ? ( ) : null}
) : null}
Performance
{VISUALIZER_FRAME_TARGETS.map((target) => ( ))}
{roundedDockedRenderFps} FPS
Trim
{inputGainDb > 0 ? '+' : ''}{inputGainDb.toFixed(1)}dB setInputGain(Number(event.target.value))} onDoubleClick={() => setInputGain(0)} />
) }