feat(library): add per-user Library tab preferences store
Mirrors the FavoritesPreferences pattern so the Local Library can have a user-configurable tab order and hidden-tabs list, persisted per user in library_preferences.db. Adds the LibraryPreferences struct, the single-row SQLite table, the LibraryPreferencesState lifecycle, and the V2 get/save commands. Default order matches the current LocalLibraryView tab list with the renamed 'folders' tab in place of the old 'albums' tab. Phase 3 will add 'albums' (the new metadata-grouped view) to this default. Frontend wiring lands in the next commit on this branch. cc
This commit is contained in:
6 files changed
+219
No files matched your search
@@ -3,6 +3,7 @@ use tauri::State;
|
||||
use crate::audio::{AlsaPlugin, AudioBackendType, AudioDevice, BackendManager};
|
||||
use crate::cache::CacheStats;
|
||||
use crate::config::favorites_preferences::FavoritesPreferences;
|
||||
use crate::config::library_preferences::LibraryPreferences;
|
||||
use crate::config::playback_preferences::{
|
||||
AutoplayMode, PlaybackPreferences, PlaybackPreferencesState,
|
||||
};
|
||||
@@ -190,6 +191,26 @@ pub fn v2_save_favorites_preferences(
|
||||
crate::config::favorites_preferences::save_favorites_preferences(prefs, state)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn v2_get_library_preferences(
|
||||
state: State<'_, crate::config::library_preferences::LibraryPreferencesState>,
|
||||
) -> Result<LibraryPreferences, String> {
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock library preferences store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.get_preferences()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn v2_save_library_preferences(
|
||||
prefs: LibraryPreferences,
|
||||
state: State<'_, crate::config::library_preferences::LibraryPreferencesState>,
|
||||
) -> Result<LibraryPreferences, String> {
|
||||
crate::config::library_preferences::save_library_preferences(prefs, state)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn v2_get_cache_stats(state: State<'_, AppState>) -> CacheStats {
|
||||
state.audio_cache.stats()
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
use rusqlite::{params, Connection, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LibraryPreferences {
|
||||
pub tab_order: Vec<String>,
|
||||
pub hidden_tabs: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for LibraryPreferences {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// Default order matches the current LocalLibraryView tab list,
|
||||
// with the renamed 'folders' tab in place of the old 'albums'
|
||||
// tab. Phase 3 adds 'albums' (the new metadata-grouped view)
|
||||
// to this default.
|
||||
tab_order: vec![
|
||||
"tracks".to_string(),
|
||||
"folders".to_string(),
|
||||
"artists".to_string(),
|
||||
],
|
||||
hidden_tabs: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LibraryPreferencesStore {
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
impl LibraryPreferencesStore {
|
||||
fn open_at(dir: &Path, db_name: &str) -> Result<Self, String> {
|
||||
std::fs::create_dir_all(dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
|
||||
let db_path = dir.join(db_name);
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open library preferences database: {}", e))?;
|
||||
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")
|
||||
.map_err(|e| format!("Failed to set WAL mode on library preferences: {}", e))?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS library_preferences (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
tab_order TEXT NOT NULL,
|
||||
hidden_tabs TEXT NOT NULL DEFAULT '[]'
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| format!("Failed to create library preferences table: {}", e))?;
|
||||
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
Self::open_at(&data_dir, "library_preferences.db")
|
||||
}
|
||||
|
||||
pub fn new_at(base_dir: &Path) -> Result<Self, String> {
|
||||
Self::open_at(base_dir, "library_preferences.db")
|
||||
}
|
||||
|
||||
pub fn get_preferences(&self) -> Result<LibraryPreferences, String> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT tab_order, hidden_tabs FROM library_preferences WHERE id = 1")
|
||||
.map_err(|e| format!("Failed to prepare select: {}", e))?;
|
||||
|
||||
let result = stmt.query_row([], |row| {
|
||||
let tab_order_str: String = row.get(0)?;
|
||||
let hidden_tabs_str: String = row.get(1)?;
|
||||
|
||||
let tab_order: Vec<String> = serde_json::from_str(&tab_order_str)
|
||||
.unwrap_or_else(|_| LibraryPreferences::default().tab_order);
|
||||
let hidden_tabs: Vec<String> =
|
||||
serde_json::from_str(&hidden_tabs_str).unwrap_or_default();
|
||||
|
||||
Ok(LibraryPreferences {
|
||||
tab_order,
|
||||
hidden_tabs,
|
||||
})
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(prefs) => Ok(prefs),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(LibraryPreferences::default()),
|
||||
Err(e) => Err(format!("Failed to query library preferences: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_preferences(
|
||||
&self,
|
||||
prefs: LibraryPreferences,
|
||||
) -> Result<LibraryPreferences, String> {
|
||||
let tab_order_str = serde_json::to_string(&prefs.tab_order)
|
||||
.map_err(|e| format!("Failed to serialize tab_order: {}", e))?;
|
||||
let hidden_tabs_str = serde_json::to_string(&prefs.hidden_tabs)
|
||||
.map_err(|e| format!("Failed to serialize hidden_tabs: {}", e))?;
|
||||
|
||||
self.conn
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO library_preferences (id, tab_order, hidden_tabs)
|
||||
VALUES (1, ?1, ?2)",
|
||||
params![tab_order_str, hidden_tabs_str],
|
||||
)
|
||||
.map_err(|e| format!("Failed to save library preferences: {}", e))?;
|
||||
Ok(prefs)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LibraryPreferencesState {
|
||||
pub store: Arc<Mutex<Option<LibraryPreferencesStore>>>,
|
||||
}
|
||||
|
||||
impl LibraryPreferencesState {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let store = LibraryPreferencesStore::new()?;
|
||||
Ok(Self {
|
||||
store: Arc::new(Mutex::new(Some(store))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
store: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_at(&self, base_dir: &Path) -> Result<(), String> {
|
||||
let new_store = LibraryPreferencesStore::new_at(base_dir)?;
|
||||
let mut guard = self
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock library preferences store".to_string())?;
|
||||
*guard = Some(new_store);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) -> Result<(), String> {
|
||||
let mut guard = self
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock library preferences store".to_string())?;
|
||||
*guard = None;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_library_preferences(
|
||||
state: tauri::State<LibraryPreferencesState>,
|
||||
) -> Result<LibraryPreferences, String> {
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock library preferences store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.get_preferences()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn save_library_preferences(
|
||||
prefs: LibraryPreferences,
|
||||
state: tauri::State<LibraryPreferencesState>,
|
||||
) -> Result<LibraryPreferences, String> {
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock library preferences store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.save_preferences(prefs)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ pub mod favorites_preferences;
|
||||
pub mod graphics_settings;
|
||||
pub mod image_cache_settings;
|
||||
pub mod legal_settings;
|
||||
pub mod library_preferences;
|
||||
pub mod playback_preferences;
|
||||
pub mod remote_control_settings;
|
||||
pub mod subscription_state;
|
||||
@@ -43,6 +44,11 @@ pub use favorites_preferences::{
|
||||
FavoritesPreferencesState,
|
||||
};
|
||||
|
||||
pub use library_preferences::{
|
||||
get_library_preferences, save_library_preferences, LibraryPreferences,
|
||||
LibraryPreferencesState,
|
||||
};
|
||||
|
||||
pub use subscription_state::{
|
||||
create_empty_subscription_state, create_subscription_state, SubscriptionState,
|
||||
SubscriptionStateState, SubscriptionStateStore,
|
||||
|
||||
@@ -704,6 +704,8 @@ pub fn run(qconnect_cli_override: Option<bool>) {
|
||||
let playback_prefs_state = config::playback_preferences::PlaybackPreferencesState::new_empty();
|
||||
let favorites_prefs_state =
|
||||
config::favorites_preferences::FavoritesPreferencesState::new_empty();
|
||||
let library_prefs_state =
|
||||
config::library_preferences::LibraryPreferencesState::new_empty();
|
||||
let favorites_cache_state = config::favorites_cache::FavoritesCacheState::new_empty();
|
||||
let tray_settings_state = config::tray_settings::TraySettingsState::new_empty();
|
||||
let remote_control_settings_state =
|
||||
@@ -1352,6 +1354,7 @@ pub fn run(qconnect_cli_override: Option<bool>) {
|
||||
.manage(offline_state)
|
||||
.manage(playback_prefs_state)
|
||||
.manage(favorites_prefs_state)
|
||||
.manage(library_prefs_state)
|
||||
.manage(favorites_cache_state)
|
||||
.manage(tray_settings_state)
|
||||
.manage(remote_control_settings_state)
|
||||
@@ -1452,6 +1455,8 @@ pub fn run(qconnect_cli_override: Option<bool>) {
|
||||
commands_v2::v2_get_playback_preferences,
|
||||
commands_v2::v2_get_favorites_preferences,
|
||||
commands_v2::v2_save_favorites_preferences,
|
||||
commands_v2::v2_get_library_preferences,
|
||||
commands_v2::v2_save_library_preferences,
|
||||
commands_v2::v2_get_cache_stats,
|
||||
commands_v2::v2_get_available_backends,
|
||||
commands_v2::v2_get_devices_for_backend,
|
||||
|
||||
@@ -16,6 +16,7 @@ const DATA_DIR_DB_FILES: &[&str] = &[
|
||||
"subscription_state.db",
|
||||
"playback_preferences.db",
|
||||
"favorites_preferences.db",
|
||||
"library_preferences.db",
|
||||
"download_settings.db",
|
||||
"audio_settings.db",
|
||||
"tray_settings.db",
|
||||
|
||||
@@ -61,6 +61,8 @@ pub async fn activate_session(app: &tauri::AppHandle, user_id: u64) -> Result<()
|
||||
app.state::<crate::config::playback_preferences::PlaybackPreferencesState>();
|
||||
let favorites_prefs =
|
||||
app.state::<crate::config::favorites_preferences::FavoritesPreferencesState>();
|
||||
let library_prefs =
|
||||
app.state::<crate::config::library_preferences::LibraryPreferencesState>();
|
||||
let download_settings = app.state::<crate::config::download_settings::DownloadSettingsState>();
|
||||
let audio_settings = app.state::<crate::config::audio_settings::AudioSettingsState>();
|
||||
let tray_settings = app.state::<crate::config::tray_settings::TraySettingsState>();
|
||||
@@ -114,6 +116,7 @@ pub async fn activate_session(app: &tauri::AppHandle, user_id: u64) -> Result<()
|
||||
favorites_cache.init_at(&data_dir)?;
|
||||
playback_prefs.init_at(&data_dir)?;
|
||||
favorites_prefs.init_at(&data_dir)?;
|
||||
library_prefs.init_at(&data_dir)?;
|
||||
audio_settings.init_at(&data_dir)?;
|
||||
|
||||
// Sync per-user audio settings to CoreBridge player immediately.
|
||||
@@ -305,6 +308,8 @@ pub async fn deactivate_session(app: &tauri::AppHandle) -> Result<(), String> {
|
||||
app.state::<crate::config::playback_preferences::PlaybackPreferencesState>();
|
||||
let favorites_prefs =
|
||||
app.state::<crate::config::favorites_preferences::FavoritesPreferencesState>();
|
||||
let library_prefs =
|
||||
app.state::<crate::config::library_preferences::LibraryPreferencesState>();
|
||||
let download_settings = app.state::<crate::config::download_settings::DownloadSettingsState>();
|
||||
let audio_settings = app.state::<crate::config::audio_settings::AudioSettingsState>();
|
||||
let tray_settings = app.state::<crate::config::tray_settings::TraySettingsState>();
|
||||
@@ -336,6 +341,7 @@ pub async fn deactivate_session(app: &tauri::AppHandle) -> Result<(), String> {
|
||||
favorites_cache.teardown()?;
|
||||
playback_prefs.teardown()?;
|
||||
favorites_prefs.teardown()?;
|
||||
library_prefs.teardown()?;
|
||||
audio_settings.teardown()?;
|
||||
tray_settings.teardown()?;
|
||||
remote_control_settings.teardown()?;
|
||||
@@ -428,6 +434,8 @@ pub async fn activate_offline_session(app: &tauri::AppHandle) -> Result<(), Stri
|
||||
let tray_settings = app.state::<crate::config::tray_settings::TraySettingsState>();
|
||||
let favorites_prefs =
|
||||
app.state::<crate::config::favorites_preferences::FavoritesPreferencesState>();
|
||||
let library_prefs =
|
||||
app.state::<crate::config::library_preferences::LibraryPreferencesState>();
|
||||
|
||||
library.init_at(&data_dir).await?;
|
||||
|
||||
@@ -450,6 +458,7 @@ pub async fn activate_offline_session(app: &tauri::AppHandle) -> Result<(), Stri
|
||||
session_store.init_at(&data_dir)?;
|
||||
favorites_cache.init_at(&data_dir)?;
|
||||
favorites_prefs.init_at(&data_dir)?;
|
||||
library_prefs.init_at(&data_dir)?;
|
||||
tray_settings.init_at(&data_dir)?;
|
||||
|
||||
// Download settings — needed for "Show in Local Library" toggle
|
||||
|
||||
Reference in new issue
Block a user