From e46c8e72fa372066e63cfb2eddaa49d045e1106c Mon Sep 17 00:00:00 2001 From: JustusPlays78 Date: Sat, 30 May 2026 04:59:20 +0200 Subject: [PATCH 1/2] feat - show configured audio devices on connect screen (#350) --- .../radio/audio-hardware-summary.tsx | 96 +++++++++++++++++++ .../src/components/radio/radio-container.tsx | 2 + .../settings-modal/settings-modal.tsx | 5 + 3 files changed, 103 insertions(+) create mode 100644 src/renderer/src/components/radio/audio-hardware-summary.tsx 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..5a5ece5 --- /dev/null +++ b/src/renderer/src/components/radio/audio-hardware-summary.tsx @@ -0,0 +1,96 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { AudioDevice } from 'trackaudio-afv'; +import { Configuration } from '../../../../shared/config.type'; + +// Dispatched on the window whenever the user changes an audio device or API in the +// settings modal, so the pre-connection hardware summary can refresh immediately. +export const AUDIO_CONFIG_CHANGED_EVENT = 'trackaudio-audio-config-changed'; + +const resolveDeviceName = (devices: AudioDevice[], deviceId: string): string | null => { + if (!deviceId) { + return null; + } + const match = devices.find((device) => device.id === deviceId); + return match ? match.name : null; +}; + +interface DeviceLineProps { + label: string; + deviceName: string | null; +} + +const DeviceLine: React.FC = ({ label, deviceName }) => ( +
+ {label}: + + {deviceName ?? 'Not configured'} + +
+); + +/** + * Shows the currently configured audio hardware on the pre-connection screen so the + * controller can verify their setup before connecting. Devices that are unset or no + * longer available are highlighted, addressing silent loss of the audio configuration. + */ +const AudioHardwareSummary: React.FC = () => { + const [inputName, setInputName] = useState(null); + const [headsetName, setHeadsetName] = useState(null); + const [speakerName, setSpeakerName] = useState(null); + + const refresh = useCallback(() => { + window.api + .getConfig() + .then((config: Configuration) => { + if (!config.audioApi && config.audioApi !== 0) { + return; + } + + window.api + .getAudioInputDevices(config.audioApi) + .then((devices: AudioDevice[]) => { + setInputName(resolveDeviceName(devices, config.audioInputDeviceId)); + }) + .catch((err: unknown) => { + console.error(err); + }); + + window.api + .getAudioOutputDevices(config.audioApi) + .then((devices: AudioDevice[]) => { + setHeadsetName(resolveDeviceName(devices, config.headsetOutputDeviceId)); + setSpeakerName(resolveDeviceName(devices, config.speakerOutputDeviceId)); + }) + .catch((err: unknown) => { + console.error(err); + }); + }) + .catch((err: unknown) => { + console.error(err); + }); + }, []); + + useEffect(() => { + refresh(); + + window.addEventListener('focus', refresh); + window.addEventListener(AUDIO_CONFIG_CHANGED_EVENT, refresh); + + return () => { + 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); }; From 612c7977f66ed27888877ba21cfba64a7eb64c1f Mon Sep 17 00:00:00 2001 From: JustusPlays78 Date: Sat, 30 May 2026 05:44:56 +0200 Subject: [PATCH 2/2] fix - harden audio device summary (#350) --- .../radio/audio-hardware-summary.tsx | 101 ++++++++++++------ 1 file changed, 70 insertions(+), 31 deletions(-) diff --git a/src/renderer/src/components/radio/audio-hardware-summary.tsx b/src/renderer/src/components/radio/audio-hardware-summary.tsx index 5a5ece5..f8b897c 100644 --- a/src/renderer/src/components/radio/audio-hardware-summary.tsx +++ b/src/renderer/src/components/radio/audio-hardware-summary.tsx @@ -1,55 +1,89 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import clsx from 'clsx'; import { AudioDevice } from 'trackaudio-afv'; import { Configuration } from '../../../../shared/config.type'; -// Dispatched on the window whenever the user changes an audio device or API in the -// settings modal, so the pre-connection hardware summary can refresh immediately. export const AUDIO_CONFIG_CHANGED_EVENT = 'trackaudio-audio-config-changed'; -const resolveDeviceName = (devices: AudioDevice[], deviceId: string): string | null => { +type DeviceStatus = + | { state: 'loading' } + | { state: 'unset' } + | { state: 'unavailable' } + | { state: 'set'; name: string }; + +const resolveDeviceStatus = (devices: AudioDevice[], deviceId: string): DeviceStatus => { if (!deviceId) { - return null; + return { state: 'unset' }; } const match = devices.find((device) => device.id === deviceId); - return match ? match.name : null; + return match ? { state: 'set', name: match.name } : { state: 'unavailable' }; }; interface DeviceLineProps { label: string; - deviceName: string | null; + status: DeviceStatus; } -const DeviceLine: React.FC = ({ label, deviceName }) => ( -
- {label}: - - {deviceName ?? 'Not configured'} - -
-); - -/** - * Shows the currently configured audio hardware on the pre-connection screen so the - * controller can verify their setup before connecting. Devices that are unset or no - * longer available are highlighted, addressing silent loss of the audio configuration. - */ +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 [inputName, setInputName] = useState(null); - const [headsetName, setHeadsetName] = useState(null); - const [speakerName, setSpeakerName] = useState(null); + 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 (!config.audioApi && config.audioApi !== 0) { + 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[]) => { - setInputName(resolveDeviceName(devices, config.audioInputDeviceId)); + if (isStale()) { + return; + } + setInputStatus(resolveDeviceStatus(devices, config.audioInputDeviceId)); }) .catch((err: unknown) => { console.error(err); @@ -58,8 +92,11 @@ const AudioHardwareSummary: React.FC = () => { window.api .getAudioOutputDevices(config.audioApi) .then((devices: AudioDevice[]) => { - setHeadsetName(resolveDeviceName(devices, config.headsetOutputDeviceId)); - setSpeakerName(resolveDeviceName(devices, config.speakerOutputDeviceId)); + if (isStale()) { + return; + } + setHeadsetStatus(resolveDeviceStatus(devices, config.headsetOutputDeviceId)); + setSpeakerStatus(resolveDeviceStatus(devices, config.speakerOutputDeviceId)); }) .catch((err: unknown) => { console.error(err); @@ -71,12 +108,14 @@ const AudioHardwareSummary: React.FC = () => { }, []); 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); }; @@ -85,9 +124,9 @@ const AudioHardwareSummary: React.FC = () => { return (
- - - + + +
);