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:
vicrodh committed 2026-05-08 14:40:47 -06:00
1 parent 24558d2a7d
commit a525bc92ec
3 files changed
+195 -5

No files matched your search

+15 -1
View File
@@ -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()
+179 -4
View File
@@ -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);
}
}
+1
View File
@@ -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,