From fc7a5815efebb5bb450520c319692234ae8846ba Mon Sep 17 00:00:00 2001 From: Psychotoxical Date: Mon, 13 Apr 2026 18:38:58 +0200 Subject: [PATCH] feat(audio): add refresh button for device list in Settings The module-level cache prevented newly connected USB audio devices from appearing in the dropdown. Replace with a manual refresh button (RotateCcw icon, spins while loading) next to the dropdown. Device enumeration now runs on every Audio tab open and on explicit refresh. ALSA stderr noise is already suppressed by the dup2 guard, so re-enumeration is silent. Co-Authored-By: Claude Sonnet 4.6 --- src/pages/Settings.tsx | 67 ++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 475ffc24..ee454550 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -221,10 +221,6 @@ function snapHotCacheMb(v: number): number { return Math.round((x - 32) / 32) * 32 + 32; } -// Module-level cache — device enumeration triggers ALSA probing on Linux (stderr noise), -// so we only enumerate once per app session. -let _audioDevicesCache: string[] | null = null; - export default function Settings() { const auth = useAuthStore(); const theme = useThemeStore(); @@ -266,6 +262,7 @@ export default function Settings() { const [hotCacheBytes, setHotCacheBytes] = useState(null); const [audioDevices, setAudioDevices] = useState([]); const [deviceSwitching, setDeviceSwitching] = useState(false); + const [devicesLoading, setDevicesLoading] = useState(false); const [showClearConfirm, setShowClearConfirm] = useState(false); const [clearing, setClearing] = useState(false); const [contributorsOpen, setContributorsOpen] = useState(false); @@ -282,15 +279,18 @@ export default function Settings() { invoke('get_hot_cache_size', { customDir: auth.hotCacheDownloadDir || null }).then(setHotCacheBytes).catch(() => setHotCacheBytes(0)); }, [activeTab, auth.offlineDownloadDir, auth.hotCacheDownloadDir]); - // Load available audio output devices when Audio tab is first opened. - // Uses a module-level cache so ALSA enumeration (noisy on Linux) only happens once per session. + const refreshAudioDevices = () => { + setDevicesLoading(true); + invoke('audio_list_devices') + .then(setAudioDevices) + .catch(() => {}) + .finally(() => setDevicesLoading(false)); + }; + + // Load available audio output devices when Audio tab opens. useEffect(() => { if (activeTab !== 'audio') return; - if (_audioDevicesCache) { setAudioDevices(_audioDevicesCache); return; } - invoke('audio_list_devices').then(devices => { - _audioDevicesCache = devices; - setAudioDevices(devices); - }).catch(() => {}); + refreshAudioDevices(); }, [activeTab]); /** Live disk usage for hot cache while Audio settings are open (interval + refresh when index changes). */ @@ -517,23 +517,34 @@ export default function Settings() {
{t('settings.audioOutputDeviceDesc')}
- { - const device = val || null; - setDeviceSwitching(true); - try { - await invoke('audio_set_device', { deviceName: device }); - auth.setAudioOutputDevice(device); - } catch { /* device open failed — don't persist */ } - setDeviceSwitching(false); - }} - options={[ - { value: '', label: t('settings.audioOutputDeviceDefault') }, - ...audioDevices.map(d => ({ value: d, label: d })), - ]} - /> +
+ { + const device = val || null; + setDeviceSwitching(true); + try { + await invoke('audio_set_device', { deviceName: device }); + auth.setAudioOutputDevice(device); + } catch { /* device open failed — don't persist */ } + setDeviceSwitching(false); + }} + options={[ + { value: '', label: t('settings.audioOutputDeviceDefault') }, + ...audioDevices.map(d => ({ value: d, label: d })), + ]} + /> + +