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::audio::{AlsaPlugin, AudioBackendType, AudioDevice, BackendManager};
|
||||||
use crate::cache::CacheStats;
|
use crate::cache::CacheStats;
|
||||||
use crate::config::favorites_preferences::FavoritesPreferences;
|
use crate::config::favorites_preferences::FavoritesPreferences;
|
||||||
|
use crate::config::library_preferences::LibraryPreferences;
|
||||||
use crate::config::playback_preferences::{
|
use crate::config::playback_preferences::{
|
||||||
AutoplayMode, PlaybackPreferences, PlaybackPreferencesState,
|
AutoplayMode, PlaybackPreferences, PlaybackPreferencesState,
|
||||||
};
|
};
|
||||||
@@ -190,6 +191,26 @@ pub fn v2_save_favorites_preferences(
|
|||||||
crate::config::favorites_preferences::save_favorites_preferences(prefs, state)
|
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]
|
#[tauri::command]
|
||||||
pub fn v2_get_cache_stats(state: State<'_, AppState>) -> CacheStats {
|
pub fn v2_get_cache_stats(state: State<'_, AppState>) -> CacheStats {
|
||||||
state.audio_cache.stats()
|
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 graphics_settings;
|
||||||
pub mod image_cache_settings;
|
pub mod image_cache_settings;
|
||||||
pub mod legal_settings;
|
pub mod legal_settings;
|
||||||
|
pub mod library_preferences;
|
||||||
pub mod playback_preferences;
|
pub mod playback_preferences;
|
||||||
pub mod remote_control_settings;
|
pub mod remote_control_settings;
|
||||||
pub mod subscription_state;
|
pub mod subscription_state;
|
||||||
@@ -43,6 +44,11 @@ pub use favorites_preferences::{
|
|||||||
FavoritesPreferencesState,
|
FavoritesPreferencesState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub use library_preferences::{
|
||||||
|
get_library_preferences, save_library_preferences, LibraryPreferences,
|
||||||
|
LibraryPreferencesState,
|
||||||
|
};
|
||||||
|
|
||||||
pub use subscription_state::{
|
pub use subscription_state::{
|
||||||
create_empty_subscription_state, create_subscription_state, SubscriptionState,
|
create_empty_subscription_state, create_subscription_state, SubscriptionState,
|
||||||
SubscriptionStateState, SubscriptionStateStore,
|
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 playback_prefs_state = config::playback_preferences::PlaybackPreferencesState::new_empty();
|
||||||
let favorites_prefs_state =
|
let favorites_prefs_state =
|
||||||
config::favorites_preferences::FavoritesPreferencesState::new_empty();
|
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 favorites_cache_state = config::favorites_cache::FavoritesCacheState::new_empty();
|
||||||
let tray_settings_state = config::tray_settings::TraySettingsState::new_empty();
|
let tray_settings_state = config::tray_settings::TraySettingsState::new_empty();
|
||||||
let remote_control_settings_state =
|
let remote_control_settings_state =
|
||||||
@@ -1352,6 +1354,7 @@ pub fn run(qconnect_cli_override: Option<bool>) {
|
|||||||
.manage(offline_state)
|
.manage(offline_state)
|
||||||
.manage(playback_prefs_state)
|
.manage(playback_prefs_state)
|
||||||
.manage(favorites_prefs_state)
|
.manage(favorites_prefs_state)
|
||||||
|
.manage(library_prefs_state)
|
||||||
.manage(favorites_cache_state)
|
.manage(favorites_cache_state)
|
||||||
.manage(tray_settings_state)
|
.manage(tray_settings_state)
|
||||||
.manage(remote_control_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_playback_preferences,
|
||||||
commands_v2::v2_get_favorites_preferences,
|
commands_v2::v2_get_favorites_preferences,
|
||||||
commands_v2::v2_save_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_cache_stats,
|
||||||
commands_v2::v2_get_available_backends,
|
commands_v2::v2_get_available_backends,
|
||||||
commands_v2::v2_get_devices_for_backend,
|
commands_v2::v2_get_devices_for_backend,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const DATA_DIR_DB_FILES: &[&str] = &[
|
|||||||
"subscription_state.db",
|
"subscription_state.db",
|
||||||
"playback_preferences.db",
|
"playback_preferences.db",
|
||||||
"favorites_preferences.db",
|
"favorites_preferences.db",
|
||||||
|
"library_preferences.db",
|
||||||
"download_settings.db",
|
"download_settings.db",
|
||||||
"audio_settings.db",
|
"audio_settings.db",
|
||||||
"tray_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>();
|
app.state::<crate::config::playback_preferences::PlaybackPreferencesState>();
|
||||||
let favorites_prefs =
|
let favorites_prefs =
|
||||||
app.state::<crate::config::favorites_preferences::FavoritesPreferencesState>();
|
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 download_settings = app.state::<crate::config::download_settings::DownloadSettingsState>();
|
||||||
let audio_settings = app.state::<crate::config::audio_settings::AudioSettingsState>();
|
let audio_settings = app.state::<crate::config::audio_settings::AudioSettingsState>();
|
||||||
let tray_settings = app.state::<crate::config::tray_settings::TraySettingsState>();
|
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)?;
|
favorites_cache.init_at(&data_dir)?;
|
||||||
playback_prefs.init_at(&data_dir)?;
|
playback_prefs.init_at(&data_dir)?;
|
||||||
favorites_prefs.init_at(&data_dir)?;
|
favorites_prefs.init_at(&data_dir)?;
|
||||||
|
library_prefs.init_at(&data_dir)?;
|
||||||
audio_settings.init_at(&data_dir)?;
|
audio_settings.init_at(&data_dir)?;
|
||||||
|
|
||||||
// Sync per-user audio settings to CoreBridge player immediately.
|
// 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>();
|
app.state::<crate::config::playback_preferences::PlaybackPreferencesState>();
|
||||||
let favorites_prefs =
|
let favorites_prefs =
|
||||||
app.state::<crate::config::favorites_preferences::FavoritesPreferencesState>();
|
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 download_settings = app.state::<crate::config::download_settings::DownloadSettingsState>();
|
||||||
let audio_settings = app.state::<crate::config::audio_settings::AudioSettingsState>();
|
let audio_settings = app.state::<crate::config::audio_settings::AudioSettingsState>();
|
||||||
let tray_settings = app.state::<crate::config::tray_settings::TraySettingsState>();
|
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()?;
|
favorites_cache.teardown()?;
|
||||||
playback_prefs.teardown()?;
|
playback_prefs.teardown()?;
|
||||||
favorites_prefs.teardown()?;
|
favorites_prefs.teardown()?;
|
||||||
|
library_prefs.teardown()?;
|
||||||
audio_settings.teardown()?;
|
audio_settings.teardown()?;
|
||||||
tray_settings.teardown()?;
|
tray_settings.teardown()?;
|
||||||
remote_control_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 tray_settings = app.state::<crate::config::tray_settings::TraySettingsState>();
|
||||||
let favorites_prefs =
|
let favorites_prefs =
|
||||||
app.state::<crate::config::favorites_preferences::FavoritesPreferencesState>();
|
app.state::<crate::config::favorites_preferences::FavoritesPreferencesState>();
|
||||||
|
let library_prefs =
|
||||||
|
app.state::<crate::config::library_preferences::LibraryPreferencesState>();
|
||||||
|
|
||||||
library.init_at(&data_dir).await?;
|
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)?;
|
session_store.init_at(&data_dir)?;
|
||||||
favorites_cache.init_at(&data_dir)?;
|
favorites_cache.init_at(&data_dir)?;
|
||||||
favorites_prefs.init_at(&data_dir)?;
|
favorites_prefs.init_at(&data_dir)?;
|
||||||
|
library_prefs.init_at(&data_dir)?;
|
||||||
tray_settings.init_at(&data_dir)?;
|
tray_settings.init_at(&data_dir)?;
|
||||||
|
|
||||||
// Download settings — needed for "Show in Local Library" toggle
|
// Download settings — needed for "Show in Local Library" toggle
|
||||||
|
|||||||
Reference in new issue
Block a user