mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
+39
-28
@@ -221,10 +221,6 @@ function snapHotCacheMb(v: number): number {
|
|||||||
return Math.round((x - 32) / 32) * 32 + 32;
|
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() {
|
export default function Settings() {
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
@@ -266,6 +262,7 @@ export default function Settings() {
|
|||||||
const [hotCacheBytes, setHotCacheBytes] = useState<number | null>(null);
|
const [hotCacheBytes, setHotCacheBytes] = useState<number | null>(null);
|
||||||
const [audioDevices, setAudioDevices] = useState<string[]>([]);
|
const [audioDevices, setAudioDevices] = useState<string[]>([]);
|
||||||
const [deviceSwitching, setDeviceSwitching] = useState(false);
|
const [deviceSwitching, setDeviceSwitching] = useState(false);
|
||||||
|
const [devicesLoading, setDevicesLoading] = useState(false);
|
||||||
const [showClearConfirm, setShowClearConfirm] = useState(false);
|
const [showClearConfirm, setShowClearConfirm] = useState(false);
|
||||||
const [clearing, setClearing] = useState(false);
|
const [clearing, setClearing] = useState(false);
|
||||||
const [contributorsOpen, setContributorsOpen] = useState(false);
|
const [contributorsOpen, setContributorsOpen] = useState(false);
|
||||||
@@ -282,15 +279,18 @@ export default function Settings() {
|
|||||||
invoke<number>('get_hot_cache_size', { customDir: auth.hotCacheDownloadDir || null }).then(setHotCacheBytes).catch(() => setHotCacheBytes(0));
|
invoke<number>('get_hot_cache_size', { customDir: auth.hotCacheDownloadDir || null }).then(setHotCacheBytes).catch(() => setHotCacheBytes(0));
|
||||||
}, [activeTab, auth.offlineDownloadDir, auth.hotCacheDownloadDir]);
|
}, [activeTab, auth.offlineDownloadDir, auth.hotCacheDownloadDir]);
|
||||||
|
|
||||||
// Load available audio output devices when Audio tab is first opened.
|
const refreshAudioDevices = () => {
|
||||||
// Uses a module-level cache so ALSA enumeration (noisy on Linux) only happens once per session.
|
setDevicesLoading(true);
|
||||||
|
invoke<string[]>('audio_list_devices')
|
||||||
|
.then(setAudioDevices)
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => setDevicesLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load available audio output devices when Audio tab opens.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTab !== 'audio') return;
|
if (activeTab !== 'audio') return;
|
||||||
if (_audioDevicesCache) { setAudioDevices(_audioDevicesCache); return; }
|
refreshAudioDevices();
|
||||||
invoke<string[]>('audio_list_devices').then(devices => {
|
|
||||||
_audioDevicesCache = devices;
|
|
||||||
setAudioDevices(devices);
|
|
||||||
}).catch(() => {});
|
|
||||||
}, [activeTab]);
|
}, [activeTab]);
|
||||||
|
|
||||||
/** Live disk usage for hot cache while Audio settings are open (interval + refresh when index changes). */
|
/** Live disk usage for hot cache while Audio settings are open (interval + refresh when index changes). */
|
||||||
@@ -517,23 +517,34 @@ export default function Settings() {
|
|||||||
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginBottom: '0.75rem' }}>
|
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginBottom: '0.75rem' }}>
|
||||||
{t('settings.audioOutputDeviceDesc')}
|
{t('settings.audioOutputDeviceDesc')}
|
||||||
</div>
|
</div>
|
||||||
<CustomSelect
|
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||||||
value={auth.audioOutputDevice ?? ''}
|
<CustomSelect
|
||||||
disabled={deviceSwitching}
|
style={{ flex: 1 }}
|
||||||
onChange={async (val) => {
|
value={auth.audioOutputDevice ?? ''}
|
||||||
const device = val || null;
|
disabled={deviceSwitching || devicesLoading}
|
||||||
setDeviceSwitching(true);
|
onChange={async (val) => {
|
||||||
try {
|
const device = val || null;
|
||||||
await invoke('audio_set_device', { deviceName: device });
|
setDeviceSwitching(true);
|
||||||
auth.setAudioOutputDevice(device);
|
try {
|
||||||
} catch { /* device open failed — don't persist */ }
|
await invoke('audio_set_device', { deviceName: device });
|
||||||
setDeviceSwitching(false);
|
auth.setAudioOutputDevice(device);
|
||||||
}}
|
} catch { /* device open failed — don't persist */ }
|
||||||
options={[
|
setDeviceSwitching(false);
|
||||||
{ value: '', label: t('settings.audioOutputDeviceDefault') },
|
}}
|
||||||
...audioDevices.map(d => ({ value: d, label: d })),
|
options={[
|
||||||
]}
|
{ value: '', label: t('settings.audioOutputDeviceDefault') },
|
||||||
/>
|
...audioDevices.map(d => ({ value: d, label: d })),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="icon-btn"
|
||||||
|
onClick={refreshAudioDevices}
|
||||||
|
disabled={devicesLoading || deviceSwitching}
|
||||||
|
data-tooltip={t('common.refresh')}
|
||||||
|
>
|
||||||
|
<RotateCcw size={15} className={devicesLoading ? 'spin' : ''} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user