Add audio settings storage and commands
Backend: - Create config/audio_settings.rs with SQLite persistence - Store output_device, exclusive_mode, dac_passthrough, sample_rate - Add Tauri commands: get_audio_settings, set_audio_* - Register state and commands in lib.rs Note: Player modification to USE these settings is pending. See HANDOFF.md for implementation details.
This commit is contained in:
1 parent
ddc9497286
commit
29079458cf
8 files changed
+220
-2
No files matched your search
@@ -0,0 +1,163 @@
|
||||
//! Audio settings persistence
|
||||
//!
|
||||
//! Stores user preferences for audio output device, exclusive mode, and DAC passthrough.
|
||||
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct AudioSettings {
|
||||
pub output_device: Option<String>, // None = system default
|
||||
pub exclusive_mode: bool,
|
||||
pub dac_passthrough: bool,
|
||||
pub preferred_sample_rate: Option<u32>, // None = auto
|
||||
}
|
||||
|
||||
pub struct AudioSettingsStore {
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
impl AudioSettingsStore {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz-nix");
|
||||
|
||||
std::fs::create_dir_all(&data_dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
|
||||
let db_path = data_dir.join("audio_settings.db");
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open audio settings database: {}", e))?;
|
||||
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS audio_settings (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
output_device TEXT,
|
||||
exclusive_mode INTEGER NOT NULL DEFAULT 0,
|
||||
dac_passthrough INTEGER NOT NULL DEFAULT 0,
|
||||
preferred_sample_rate INTEGER
|
||||
);
|
||||
INSERT OR IGNORE INTO audio_settings (id, exclusive_mode, dac_passthrough)
|
||||
VALUES (1, 0, 0);"
|
||||
).map_err(|e| format!("Failed to create audio settings table: {}", e))?;
|
||||
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
pub fn get_settings(&self) -> Result<AudioSettings, String> {
|
||||
self.conn
|
||||
.query_row(
|
||||
"SELECT output_device, exclusive_mode, dac_passthrough, preferred_sample_rate FROM audio_settings WHERE id = 1",
|
||||
[],
|
||||
|row| {
|
||||
Ok(AudioSettings {
|
||||
output_device: row.get(0)?,
|
||||
exclusive_mode: row.get::<_, i64>(1)? != 0,
|
||||
dac_passthrough: row.get::<_, i64>(2)? != 0,
|
||||
preferred_sample_rate: row.get(3)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("Failed to get audio settings: {}", e))
|
||||
}
|
||||
|
||||
pub fn set_output_device(&self, device: Option<&str>) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute(
|
||||
"UPDATE audio_settings SET output_device = ?1 WHERE id = 1",
|
||||
params![device],
|
||||
)
|
||||
.map_err(|e| format!("Failed to set output device: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_exclusive_mode(&self, enabled: bool) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute(
|
||||
"UPDATE audio_settings SET exclusive_mode = ?1 WHERE id = 1",
|
||||
params![enabled as i64],
|
||||
)
|
||||
.map_err(|e| format!("Failed to set exclusive mode: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_dac_passthrough(&self, enabled: bool) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute(
|
||||
"UPDATE audio_settings SET dac_passthrough = ?1 WHERE id = 1",
|
||||
params![enabled as i64],
|
||||
)
|
||||
.map_err(|e| format!("Failed to set DAC passthrough: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_sample_rate(&self, rate: Option<u32>) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute(
|
||||
"UPDATE audio_settings SET preferred_sample_rate = ?1 WHERE id = 1",
|
||||
params![rate.map(|r| r as i64)],
|
||||
)
|
||||
.map_err(|e| format!("Failed to set sample rate: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread-safe wrapper
|
||||
pub struct AudioSettingsState {
|
||||
pub store: Arc<Mutex<AudioSettingsStore>>,
|
||||
}
|
||||
|
||||
impl AudioSettingsState {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
store: Arc::new(Mutex::new(AudioSettingsStore::new()?)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Tauri commands
|
||||
#[tauri::command]
|
||||
pub fn get_audio_settings(
|
||||
state: tauri::State<'_, AudioSettingsState>,
|
||||
) -> Result<AudioSettings, String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
store.get_settings()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_audio_output_device(
|
||||
state: tauri::State<'_, AudioSettingsState>,
|
||||
device: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
store.set_output_device(device.as_deref())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_audio_exclusive_mode(
|
||||
state: tauri::State<'_, AudioSettingsState>,
|
||||
enabled: bool,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
store.set_exclusive_mode(enabled)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_audio_dac_passthrough(
|
||||
state: tauri::State<'_, AudioSettingsState>,
|
||||
enabled: bool,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
store.set_dac_passthrough(enabled)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_audio_sample_rate(
|
||||
state: tauri::State<'_, AudioSettingsState>,
|
||||
rate: Option<u32>,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
store.set_sample_rate(rate)
|
||||
}
|
||||
@@ -7,5 +7,14 @@
|
||||
//! - Local playlists
|
||||
//! - Cached favorites
|
||||
|
||||
// TODO: Implement configuration storage
|
||||
// pub mod store;
|
||||
pub mod audio_settings;
|
||||
|
||||
pub use audio_settings::{
|
||||
AudioSettings,
|
||||
AudioSettingsState,
|
||||
get_audio_settings,
|
||||
set_audio_output_device,
|
||||
set_audio_exclusive_mode,
|
||||
set_audio_dac_passthrough,
|
||||
set_audio_sample_rate,
|
||||
};
|
||||
@@ -124,6 +124,9 @@ pub fn run() {
|
||||
// Initialize session store state
|
||||
let session_store_state = session_store::SessionStoreState::new()
|
||||
.expect("Failed to initialize session store");
|
||||
// Initialize audio settings state
|
||||
let audio_settings_state = config::audio_settings::AudioSettingsState::new()
|
||||
.expect("Failed to initialize audio settings");
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
@@ -184,6 +187,7 @@ pub fn run() {
|
||||
.manage(reco_state)
|
||||
.manage(api_cache_state)
|
||||
.manage(session_store_state)
|
||||
.manage(audio_settings_state)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// Auth commands
|
||||
commands::init_client,
|
||||
@@ -359,6 +363,12 @@ pub fn run() {
|
||||
session_store::save_session_position,
|
||||
session_store::save_session_playback_mode,
|
||||
session_store::clear_session,
|
||||
// Audio settings commands
|
||||
config::audio_settings::get_audio_settings,
|
||||
config::audio_settings::set_audio_output_device,
|
||||
config::audio_settings::set_audio_exclusive_mode,
|
||||
config::audio_settings::set_audio_dac_passthrough,
|
||||
config::audio_settings::set_audio_sample_rate,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="Apple Music" role="img"
|
||||
viewBox="0 0 512 512"><rect
|
||||
width="512" height="512"
|
||||
rx="15%" fill="url(#g)"/><linearGradient id="g" x1=".5" y1=".99" x2=".5" y2=".02"><stop offset="0" stop-color="#FA233B"/><stop offset="1" stop-color="#FB5C74"/></linearGradient><path fill="#ffffff" d="M199 359V199q0-9 10-11l138-28q11-2 12 10v122q0 15-45 20c-57 9-48 105 30 79 30-11 35-40 35-69V88s0-20-17-15l-170 35s-13 2-13 18v203q0 15-45 20c-57 9-48 105 30 79 30-11 35-40 35-69"/></svg>
|
||||
|
After Width: | Height: | Size: 636 B |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="Deezer" role="img"
|
||||
fill="none" stroke-width="20"
|
||||
viewBox="0 0 512 512"><rect
|
||||
width="512" height="512"
|
||||
rx="15%"
|
||||
fill="#231f20"/><path stroke="#f1796e" d="M78 241h64m-64 26h64"/><path stroke="#ec2529" d="M78 293h64m-64 26h64m-64 26h64"/><path stroke="#f6dd05" d="M148 293h64m-64 26h64m-64 26h64"/><path stroke="#e78dcc" d="M218 241h64m-64 26h64m-64-52h64m-64-26h64"/><path stroke="#d81996" d="M218 293h64m-64 26h64m-64 26h64"/><path stroke="#d6e465" d="M288 241h64m-64 26h64"/><path stroke="#adda1a" d="M288 293h64m-64 26h64m-64 26h64"/><path stroke="#b0d8e6" d="M358 241h64m-64 26h64m-64-52h64m-64-26h64m-64-26h64"/><path stroke="#56bbe9" d="M358 293h64m-64 26h64m-64 26h64"/></svg>
|
||||
|
After Width: | Height: | Size: 852 B |
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 192 192" fill="none"><path stroke="#000000" stroke-linecap="round" stroke-width="8" d="M140.672 157.486a76.004 76.004 0 0 1-102.464-12.128 76.001 76.001 0 0 1 107.15-107.15 76.004 76.004 0 0 1 12.128 102.464"/><path stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="15" stroke-width="8" d="M124.807 142.105a54.364 54.364 0 1 1 17.296-17.296"/><path stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="15" stroke-width="8" d="M95.997 117.193c11.705 0 21.193-9.488 21.193-21.193 0-11.704-9.488-21.193-21.193-21.193-11.704 0-21.193 9.489-21.193 21.193 0 11.705 9.489 21.193 21.193 21.193Z"/><path fill="#000000" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="15" stroke-width="3.799" d="M95.997 98.103a2.103 2.103 0 1 0 0-4.205 2.103 2.103 0 0 0 0 4.205Z"/><path fill="#000000" d="m121.193 121.196 41.871 41.625Z"/><path stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="15" stroke-width="12" d="m121.193 121.196 41.871 41.625"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
␍<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="Spotify" role="img"
|
||||
viewBox="0 0 512 512">
|
||||
␍<rect
|
||||
width="512" height="512"
|
||||
rx="15%"
|
||||
fill="#3bd75f"/>
|
||||
␍<circle cx="256" cy="256" fill="#ffffff" r="192"/>
|
||||
␍<g fill="none" stroke="#3bd75f" stroke-linecap="round">
|
||||
␍<path d="m141 195c75-20 164-15 238 24" stroke-width="36"/>
|
||||
␍<path d="m152 257c61-17 144-13 203 24" stroke-width="31"/>
|
||||
␍<path d="m156 315c54-12 116-17 178 20" stroke-width="24"/>
|
||||
␍</g>
|
||||
␍</svg>
|
||||
|
After Width: | Height: | Size: 576 B |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.016 5.323l-5.339 5.339-5.339-5.339-5.339 5.339 5.339 5.339 5.339-5.339 5.339 5.339-5.339 5.339 5.339 5.339 5.339-5.339-5.339-5.339 5.339-5.339zM21.391 10.661l5.302-5.307 5.307 5.307-5.307 5.307z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 443 B |
Reference in new issue
Block a user