feat(library-prefs): add folders_view_mode for tree/flat selection
New persisted field on LibraryPreferences gating the upcoming Tree sub-mode in the LocalLibrary Folders tab. Default flat preserves existing behaviour for current installs. Migration adds the column non-destructively. Setter v2_set_library_folders_view_mode exposes the toggle to the frontend; the existing v2_get_library_preferences returns the new field automatically via serde. cc
This commit is contained in:
3 files changed
+195
-5
No files matched your search
@@ -3,7 +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::library_preferences::{FoldersViewMode, LibraryPreferences};
|
||||||
use crate::config::playback_preferences::{
|
use crate::config::playback_preferences::{
|
||||||
AutoplayMode, PlaybackPreferences, PlaybackPreferencesState,
|
AutoplayMode, PlaybackPreferences, PlaybackPreferencesState,
|
||||||
};
|
};
|
||||||
@@ -211,6 +211,20 @@ pub fn v2_save_library_preferences(
|
|||||||
crate::config::library_preferences::save_library_preferences(prefs, state)
|
crate::config::library_preferences::save_library_preferences(prefs, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn v2_set_library_folders_view_mode(
|
||||||
|
mode: String,
|
||||||
|
state: State<'_, crate::config::library_preferences::LibraryPreferencesState>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let parsed = FoldersViewMode::from_str(&mode);
|
||||||
|
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.set_folders_view_mode(parsed)
|
||||||
|
}
|
||||||
|
|
||||||
#[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()
|
||||||
|
|||||||
@@ -3,10 +3,41 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum FoldersViewMode {
|
||||||
|
Flat,
|
||||||
|
Tree,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FoldersViewMode {
|
||||||
|
fn default() -> Self {
|
||||||
|
FoldersViewMode::Flat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FoldersViewMode {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
FoldersViewMode::Flat => "flat",
|
||||||
|
FoldersViewMode::Tree => "tree",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_str(s: &str) -> Self {
|
||||||
|
match s {
|
||||||
|
"tree" => FoldersViewMode::Tree,
|
||||||
|
_ => FoldersViewMode::Flat,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LibraryPreferences {
|
pub struct LibraryPreferences {
|
||||||
pub tab_order: Vec<String>,
|
pub tab_order: Vec<String>,
|
||||||
pub hidden_tabs: Vec<String>,
|
pub hidden_tabs: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub folders_view_mode: FoldersViewMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LibraryPreferences {
|
impl Default for LibraryPreferences {
|
||||||
@@ -23,6 +54,7 @@ impl Default for LibraryPreferences {
|
|||||||
"artists".to_string(),
|
"artists".to_string(),
|
||||||
],
|
],
|
||||||
hidden_tabs: vec![],
|
hidden_tabs: vec![],
|
||||||
|
folders_view_mode: FoldersViewMode::Flat,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,6 +84,14 @@ impl LibraryPreferencesStore {
|
|||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to create library preferences table: {}", e))?;
|
.map_err(|e| format!("Failed to create library preferences table: {}", e))?;
|
||||||
|
|
||||||
|
// Idempotent migrations for columns added after the initial schema.
|
||||||
|
// These ALTERs fail when the column already exists; that failure is
|
||||||
|
// expected and ignored.
|
||||||
|
let _ = conn.execute(
|
||||||
|
"ALTER TABLE library_preferences ADD COLUMN folders_view_mode TEXT DEFAULT 'flat'",
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
Ok(Self { conn })
|
Ok(Self { conn })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,21 +109,29 @@ impl LibraryPreferencesStore {
|
|||||||
pub fn get_preferences(&self) -> Result<LibraryPreferences, String> {
|
pub fn get_preferences(&self) -> Result<LibraryPreferences, String> {
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.conn
|
.conn
|
||||||
.prepare("SELECT tab_order, hidden_tabs FROM library_preferences WHERE id = 1")
|
.prepare(
|
||||||
|
"SELECT tab_order, hidden_tabs, folders_view_mode \
|
||||||
|
FROM library_preferences WHERE id = 1",
|
||||||
|
)
|
||||||
.map_err(|e| format!("Failed to prepare select: {}", e))?;
|
.map_err(|e| format!("Failed to prepare select: {}", e))?;
|
||||||
|
|
||||||
let result = stmt.query_row([], |row| {
|
let result = stmt.query_row([], |row| {
|
||||||
let tab_order_str: String = row.get(0)?;
|
let tab_order_str: String = row.get(0)?;
|
||||||
let hidden_tabs_str: String = row.get(1)?;
|
let hidden_tabs_str: String = row.get(1)?;
|
||||||
|
let folders_view_mode_str: Option<String> = row.get(2)?;
|
||||||
|
|
||||||
let tab_order: Vec<String> = serde_json::from_str(&tab_order_str)
|
let tab_order: Vec<String> = serde_json::from_str(&tab_order_str)
|
||||||
.unwrap_or_else(|_| LibraryPreferences::default().tab_order);
|
.unwrap_or_else(|_| LibraryPreferences::default().tab_order);
|
||||||
let hidden_tabs: Vec<String> =
|
let hidden_tabs: Vec<String> =
|
||||||
serde_json::from_str(&hidden_tabs_str).unwrap_or_default();
|
serde_json::from_str(&hidden_tabs_str).unwrap_or_default();
|
||||||
|
let folders_view_mode = folders_view_mode_str
|
||||||
|
.map(|s| FoldersViewMode::from_str(&s))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
Ok(LibraryPreferences {
|
Ok(LibraryPreferences {
|
||||||
tab_order,
|
tab_order,
|
||||||
hidden_tabs,
|
hidden_tabs,
|
||||||
|
folders_view_mode,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -105,13 +153,39 @@ impl LibraryPreferencesStore {
|
|||||||
|
|
||||||
self.conn
|
self.conn
|
||||||
.execute(
|
.execute(
|
||||||
"INSERT OR REPLACE INTO library_preferences (id, tab_order, hidden_tabs)
|
"INSERT OR REPLACE INTO library_preferences \
|
||||||
VALUES (1, ?1, ?2)",
|
(id, tab_order, hidden_tabs, folders_view_mode) \
|
||||||
params![tab_order_str, hidden_tabs_str],
|
VALUES (1, ?1, ?2, ?3)",
|
||||||
|
params![
|
||||||
|
tab_order_str,
|
||||||
|
hidden_tabs_str,
|
||||||
|
prefs.folders_view_mode.as_str()
|
||||||
|
],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to save library preferences: {}", e))?;
|
.map_err(|e| format!("Failed to save library preferences: {}", e))?;
|
||||||
Ok(prefs)
|
Ok(prefs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_folders_view_mode(&self, mode: FoldersViewMode) -> Result<(), String> {
|
||||||
|
// Ensure the row exists before updating; if no preferences have been
|
||||||
|
// saved yet, fall back to inserting a default row carrying the new
|
||||||
|
// folders_view_mode value.
|
||||||
|
let updated = self
|
||||||
|
.conn
|
||||||
|
.execute(
|
||||||
|
"UPDATE library_preferences SET folders_view_mode = ?1 WHERE id = 1",
|
||||||
|
params![mode.as_str()],
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("Failed to set folders_view_mode: {}", e))?;
|
||||||
|
|
||||||
|
if updated == 0 {
|
||||||
|
let mut prefs = LibraryPreferences::default();
|
||||||
|
prefs.folders_view_mode = mode;
|
||||||
|
self.save_preferences(prefs)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LibraryPreferencesState {
|
pub struct LibraryPreferencesState {
|
||||||
@@ -176,3 +250,104 @@ pub fn save_library_preferences(
|
|||||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||||
store.save_preferences(prefs)
|
store.save_preferences(prefs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
fn fresh_store() -> (tempfile::TempDir, LibraryPreferencesStore) {
|
||||||
|
let dir = tempdir().expect("tempdir");
|
||||||
|
let store =
|
||||||
|
LibraryPreferencesStore::new_at(dir.path()).expect("open store in temp dir");
|
||||||
|
(dir, store)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_is_flat() {
|
||||||
|
let prefs = LibraryPreferences::default();
|
||||||
|
assert_eq!(prefs.folders_view_mode, FoldersViewMode::Flat);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn legacy_json_without_folders_view_mode_defaults_to_flat() {
|
||||||
|
let legacy = r#"{
|
||||||
|
"tab_order": ["tracks", "folders", "albums", "artists"],
|
||||||
|
"hidden_tabs": []
|
||||||
|
}"#;
|
||||||
|
let prefs: LibraryPreferences =
|
||||||
|
serde_json::from_str(legacy).expect("parse legacy json");
|
||||||
|
assert_eq!(prefs.folders_view_mode, FoldersViewMode::Flat);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn round_trip_with_tree_set() {
|
||||||
|
let mut prefs = LibraryPreferences::default();
|
||||||
|
prefs.folders_view_mode = FoldersViewMode::Tree;
|
||||||
|
let json = serde_json::to_string(&prefs).expect("serialize");
|
||||||
|
let parsed: LibraryPreferences =
|
||||||
|
serde_json::from_str(&json).expect("deserialize");
|
||||||
|
assert_eq!(parsed.folders_view_mode, FoldersViewMode::Tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn folders_view_mode_str_round_trip() {
|
||||||
|
assert_eq!(FoldersViewMode::Flat.as_str(), "flat");
|
||||||
|
assert_eq!(FoldersViewMode::Tree.as_str(), "tree");
|
||||||
|
assert_eq!(FoldersViewMode::from_str("flat"), FoldersViewMode::Flat);
|
||||||
|
assert_eq!(FoldersViewMode::from_str("tree"), FoldersViewMode::Tree);
|
||||||
|
assert_eq!(
|
||||||
|
FoldersViewMode::from_str("garbage"),
|
||||||
|
FoldersViewMode::Flat
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fresh_store_returns_default_folders_view_mode() {
|
||||||
|
let (_dir, store) = fresh_store();
|
||||||
|
let prefs = store.get_preferences().expect("get prefs");
|
||||||
|
assert_eq!(prefs.folders_view_mode, FoldersViewMode::Flat);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_folders_view_mode_persists_on_empty_table() {
|
||||||
|
let (_dir, store) = fresh_store();
|
||||||
|
store
|
||||||
|
.set_folders_view_mode(FoldersViewMode::Tree)
|
||||||
|
.expect("set mode");
|
||||||
|
let prefs = store.get_preferences().expect("get prefs");
|
||||||
|
assert_eq!(prefs.folders_view_mode, FoldersViewMode::Tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_folders_view_mode_updates_existing_row() {
|
||||||
|
let (_dir, store) = fresh_store();
|
||||||
|
|
||||||
|
// Seed a row with the default save path so the UPDATE branch runs.
|
||||||
|
let mut seeded = LibraryPreferences::default();
|
||||||
|
seeded.tab_order = vec!["tracks".into(), "albums".into()];
|
||||||
|
store.save_preferences(seeded).expect("save seed");
|
||||||
|
|
||||||
|
store
|
||||||
|
.set_folders_view_mode(FoldersViewMode::Tree)
|
||||||
|
.expect("set mode");
|
||||||
|
let after = store.get_preferences().expect("get prefs");
|
||||||
|
assert_eq!(after.folders_view_mode, FoldersViewMode::Tree);
|
||||||
|
// Sibling fields preserved.
|
||||||
|
assert_eq!(
|
||||||
|
after.tab_order,
|
||||||
|
vec!["tracks".to_string(), "albums".to_string()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_preferences_persists_folders_view_mode() {
|
||||||
|
let (_dir, store) = fresh_store();
|
||||||
|
let mut prefs = LibraryPreferences::default();
|
||||||
|
prefs.folders_view_mode = FoldersViewMode::Tree;
|
||||||
|
store.save_preferences(prefs).expect("save");
|
||||||
|
|
||||||
|
let loaded = store.get_preferences().expect("load");
|
||||||
|
assert_eq!(loaded.folders_view_mode, FoldersViewMode::Tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1586,6 +1586,7 @@ pub fn run(qconnect_cli_override: Option<bool>) {
|
|||||||
commands_v2::v2_save_favorites_preferences,
|
commands_v2::v2_save_favorites_preferences,
|
||||||
commands_v2::v2_get_library_preferences,
|
commands_v2::v2_get_library_preferences,
|
||||||
commands_v2::v2_save_library_preferences,
|
commands_v2::v2_save_library_preferences,
|
||||||
|
commands_v2::v2_set_library_folders_view_mode,
|
||||||
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,
|
||||||
|
|||||||
Reference in new issue
Block a user