Add audio device enumeration and streaming quality persistence

- Add get_audio_devices command to list available output devices
- Show real audio devices in Settings dropdown
- Persist streaming quality preference in localStorage
- Persist 'prefer highest available' setting
This commit is contained in:
vicrodh committed 2026-01-09 11:13:43 -06:00
1 parent 9b8e1352eb
commit 9d040fee55
3 files changed
+89 -3

No files matched your search

+35
View File
@@ -239,3 +239,38 @@ pub fn seek(position: u64, state: State<'_, AppState>) -> Result<(), String> {
pub fn get_playback_state(state: State<'_, AppState>) -> Result<PlaybackState, String> { pub fn get_playback_state(state: State<'_, AppState>) -> Result<PlaybackState, String> {
state.player.get_state() state.player.get_state()
} }
/// Audio device information
#[derive(serde::Serialize)]
pub struct AudioDevice {
pub name: String,
pub is_default: bool,
}
/// Get available audio output devices
#[tauri::command]
pub fn get_audio_devices() -> Result<Vec<AudioDevice>, String> {
log::info!("Command: get_audio_devices");
use rodio::cpal::traits::{DeviceTrait, HostTrait};
let host = rodio::cpal::default_host();
let default_device_name = host
.default_output_device()
.and_then(|d| d.name().ok());
let devices: Vec<AudioDevice> = host
.output_devices()
.map_err(|e| format!("Failed to enumerate devices: {}", e))?
.filter_map(|device| {
device.name().ok().map(|name| {
let is_default = default_device_name.as_ref().map(|d| d == &name).unwrap_or(false);
AudioDevice { name, is_default }
})
})
.collect();
log::info!("Found {} audio output devices", devices.len());
Ok(devices)
}
+1
View File
@@ -100,6 +100,7 @@ pub fn run() {
commands::seek, commands::seek,
commands::get_playback_state, commands::get_playback_state,
commands::set_media_metadata, commands::set_media_metadata,
commands::get_audio_devices,
// Queue commands // Queue commands
commands::add_to_queue, commands::add_to_queue,
commands::add_tracks_to_queue, commands::add_tracks_to_queue,
+53 -3
View File
@@ -22,12 +22,21 @@
fetching_count: number; fetching_count: number;
} }
interface AudioDevice {
name: string;
is_default: boolean;
}
let { onBack, onLogout, userName = 'User', userEmail = '', subscription = 'Qobuz' }: Props = $props(); let { onBack, onLogout, userName = 'User', userEmail = '', subscription = 'Qobuz' }: Props = $props();
// Cache state // Cache state
let cacheStats = $state<CacheStats | null>(null); let cacheStats = $state<CacheStats | null>(null);
let isClearing = $state(false); let isClearing = $state(false);
// Audio device state
let audioDevices = $state<AudioDevice[]>([]);
let audioDeviceOptions = $derived(['System Default', ...audioDevices.map(d => d.name)]);
// Theme mapping: display name -> data-theme value // Theme mapping: display name -> data-theme value
const themeMap: Record<string, string> = { const themeMap: Record<string, string> = {
'Dark': '', 'Dark': '',
@@ -65,14 +74,55 @@
// Load saved settings on mount // Load saved settings on mount
onMount(() => { onMount(() => {
// Load theme
const savedTheme = localStorage.getItem('qbz-theme') || ''; const savedTheme = localStorage.getItem('qbz-theme') || '';
theme = themeReverseMap[savedTheme] || 'Dark'; theme = themeReverseMap[savedTheme] || 'Dark';
applyTheme(savedTheme); applyTheme(savedTheme);
// Load streaming quality preference
const savedQuality = localStorage.getItem('qbz-streaming-quality');
if (savedQuality) {
streamingQuality = savedQuality;
}
// Load prefer highest setting
const savedPreferHighest = localStorage.getItem('qbz-prefer-highest');
if (savedPreferHighest !== null) {
preferHighest = savedPreferHighest === 'true';
}
// Load cache stats // Load cache stats
loadCacheStats(); loadCacheStats();
// Load audio devices
loadAudioDevices();
}); });
function handleQualityChange(quality: string) {
streamingQuality = quality;
localStorage.setItem('qbz-streaming-quality', quality);
}
function handlePreferHighestChange(enabled: boolean) {
preferHighest = enabled;
localStorage.setItem('qbz-prefer-highest', String(enabled));
}
async function loadAudioDevices() {
try {
const devices = await invoke<AudioDevice[]>('get_audio_devices');
audioDevices = devices;
// Set current device to the default one
const defaultDevice = devices.find(d => d.is_default);
if (defaultDevice) {
outputDevice = defaultDevice.name;
}
} catch (err) {
console.error('Failed to load audio devices:', err);
}
}
async function loadCacheStats() { async function loadCacheStats() {
try { try {
cacheStats = await invoke<CacheStats>('get_cache_stats'); cacheStats = await invoke<CacheStats>('get_cache_stats');
@@ -154,18 +204,18 @@
<Dropdown <Dropdown
value={streamingQuality} value={streamingQuality}
options={['MP3', 'CD Quality', 'Hi-Res', 'Hi-Res+']} options={['MP3', 'CD Quality', 'Hi-Res', 'Hi-Res+']}
onchange={(v) => (streamingQuality = v)} onchange={handleQualityChange}
/> />
</div> </div>
<div class="setting-row"> <div class="setting-row">
<span class="setting-label">Prefer highest available</span> <span class="setting-label">Prefer highest available</span>
<Toggle enabled={preferHighest} onchange={(v) => (preferHighest = v)} /> <Toggle enabled={preferHighest} onchange={handlePreferHighestChange} />
</div> </div>
<div class="setting-row"> <div class="setting-row">
<span class="setting-label">Output Device</span> <span class="setting-label">Output Device</span>
<Dropdown <Dropdown
value={outputDevice} value={outputDevice}
options={['System Default', 'Built-in Speakers', 'USB DAC', 'HDMI Output']} options={audioDeviceOptions.length > 1 ? audioDeviceOptions : ['System Default']}
onchange={(v) => (outputDevice = v)} onchange={(v) => (outputDevice = v)}
/> />
</div> </div>