diff --git a/src/renderer/src/components/radio/audio-hardware-summary.tsx b/src/renderer/src/components/radio/audio-hardware-summary.tsx new file mode 100644 index 0000000..f8b897c --- /dev/null +++ b/src/renderer/src/components/radio/audio-hardware-summary.tsx @@ -0,0 +1,135 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import clsx from 'clsx'; +import { AudioDevice } from 'trackaudio-afv'; +import { Configuration } from '../../../../shared/config.type'; + +export const AUDIO_CONFIG_CHANGED_EVENT = 'trackaudio-audio-config-changed'; + +type DeviceStatus = + | { state: 'loading' } + | { state: 'unset' } + | { state: 'unavailable' } + | { state: 'set'; name: string }; + +const resolveDeviceStatus = (devices: AudioDevice[], deviceId: string): DeviceStatus => { + if (!deviceId) { + return { state: 'unset' }; + } + const match = devices.find((device) => device.id === deviceId); + return match ? { state: 'set', name: match.name } : { state: 'unavailable' }; +}; + +interface DeviceLineProps { + label: string; + status: DeviceStatus; +} + +const DeviceLine: React.FC = ({ label, status }) => { + const isProblem = status.state === 'unset' || status.state === 'unavailable'; + const isLoading = status.state === 'loading'; + + let text: string; + if (status.state === 'set') { + text = status.name; + } else if (status.state === 'unavailable') { + text = 'Unavailable'; + } else if (status.state === 'unset') { + text = 'Not configured'; + } else { + text = '…'; + } + + return ( +
+ {label}: + + {text} + +
+ ); +}; + +const AudioHardwareSummary: React.FC = () => { + const [inputStatus, setInputStatus] = useState({ state: 'loading' }); + const [headsetStatus, setHeadsetStatus] = useState({ state: 'loading' }); + const [speakerStatus, setSpeakerStatus] = useState({ state: 'loading' }); + const isActive = useRef(true); + const requestId = useRef(0); + + const refresh = useCallback(() => { + const thisRequest = ++requestId.current; + const isStale = (): boolean => !isActive.current || thisRequest !== requestId.current; + + window.api + .getConfig() + .then((config: Configuration) => { + if (isStale()) { + return; + } + + if (config.audioApi < 0) { + setInputStatus({ state: 'unset' }); + setHeadsetStatus({ state: 'unset' }); + setSpeakerStatus({ state: 'unset' }); + return; + } + + window.api + .getAudioInputDevices(config.audioApi) + .then((devices: AudioDevice[]) => { + if (isStale()) { + return; + } + setInputStatus(resolveDeviceStatus(devices, config.audioInputDeviceId)); + }) + .catch((err: unknown) => { + console.error(err); + }); + + window.api + .getAudioOutputDevices(config.audioApi) + .then((devices: AudioDevice[]) => { + if (isStale()) { + return; + } + setHeadsetStatus(resolveDeviceStatus(devices, config.headsetOutputDeviceId)); + setSpeakerStatus(resolveDeviceStatus(devices, config.speakerOutputDeviceId)); + }) + .catch((err: unknown) => { + console.error(err); + }); + }) + .catch((err: unknown) => { + console.error(err); + }); + }, []); + + useEffect(() => { + isActive.current = true; + refresh(); + + window.addEventListener('focus', refresh); + window.addEventListener(AUDIO_CONFIG_CHANGED_EVENT, refresh); + + return () => { + isActive.current = false; + window.removeEventListener('focus', refresh); + window.removeEventListener(AUDIO_CONFIG_CHANGED_EVENT, refresh); + }; + }, [refresh]); + + return ( +
+
+ + + +
+
+ ); +}; + +export default AudioHardwareSummary; diff --git a/src/renderer/src/components/radio/radio-container.tsx b/src/renderer/src/components/radio/radio-container.tsx index 94b010e..bff45b3 100644 --- a/src/renderer/src/components/radio/radio-container.tsx +++ b/src/renderer/src/components/radio/radio-container.tsx @@ -7,6 +7,7 @@ import useUtilStore from '@renderer/store/utilStore'; import ExpandedRxInfo from './expanded-rx-info'; import AddStation from '../sidebar/add-station'; import AddFrequency from '../sidebar/add-frequency'; +import AudioHardwareSummary from './audio-hardware-summary'; const EXCLUDED_FREQUENCIES = [0]; const UNICOM_EXCLUDED_FREQUENCIES = [122.8e6, 121.5e6]; @@ -30,6 +31,7 @@ const WaitingForConnectionMessage = () => (
Click the connect button to establish a connection to the VATSIM audio network.
+ ); diff --git a/src/renderer/src/components/settings-modal/settings-modal.tsx b/src/renderer/src/components/settings-modal/settings-modal.tsx index 14ba403..6879b66 100644 --- a/src/renderer/src/components/settings-modal/settings-modal.tsx +++ b/src/renderer/src/components/settings-modal/settings-modal.tsx @@ -9,6 +9,7 @@ import useUtilStore from '../../store/utilStore'; import AudioApis from './audio-apis'; import AudioInput from './audio-input'; import AudioOutputs from './audio-outputs'; +import { AUDIO_CONFIG_CHANGED_EVENT } from '../radio/audio-hardware-summary'; import { Info } from 'lucide-react'; import { Nav, OverlayTrigger, Tab, Tooltip } from 'react-bootstrap'; export interface SettingsModalProps { @@ -177,6 +178,7 @@ const SettingsModal: React.FC = ({ closeModal }) => { .catch(() => { setAudioInputDevices([]); }); + window.dispatchEvent(new Event(AUDIO_CONFIG_CHANGED_EVENT)); setChangesSaved(SaveStatus.Saved); }; @@ -184,6 +186,7 @@ const SettingsModal: React.FC = ({ closeModal }) => { setChangesSaved(SaveStatus.Saving); void window.api.setAudioInputDevice(deviceId); setConfig({ ...config, audioInputDeviceId: deviceId }); + window.dispatchEvent(new Event(AUDIO_CONFIG_CHANGED_EVENT)); setChangesSaved(SaveStatus.Saved); }; @@ -191,6 +194,7 @@ const SettingsModal: React.FC = ({ closeModal }) => { setChangesSaved(SaveStatus.Saving); void window.api.setHeadsetOutputDevice(deviceId); setConfig({ ...config, headsetOutputDeviceId: deviceId }); + window.dispatchEvent(new Event(AUDIO_CONFIG_CHANGED_EVENT)); setChangesSaved(SaveStatus.Saved); }; @@ -198,6 +202,7 @@ const SettingsModal: React.FC = ({ closeModal }) => { setChangesSaved(SaveStatus.Saving); void window.api.setSpeakerOutputDevice(deviceId); setConfig({ ...config, speakerOutputDeviceId: deviceId }); + window.dispatchEvent(new Event(AUDIO_CONFIG_CHANGED_EVENT)); setChangesSaved(SaveStatus.Saved); };