feat(slint): persist favorites toolbar choices across restarts

New favorites_prefs module stores albums view/sort/group + tracks group
in <data-dir>/qbz/favorites_ui.json (search stays transient). Loaded into
FavoritesState on each favorites navigation (before the data applies),
saved on every toolbar change; apply_favorites now re-derives on load so
the restored sort/group take effect (also fixes a latent case where a set
group mode wasn't applied after a tab reload). Album view toggle routes
through an albums-set-view action to persist it.

cc
This commit is contained in:
vicrodh committed 2026-05-27 08:27:34 -06:00
1 parent 4dddd36073
commit 8355f18e0a
5 files changed
+114 -4

No files matched your search

+4 -2
View File
@@ -367,6 +367,8 @@ pub fn apply_favorites(window: &AppWindow, data: FavData) {
state.set_tracks_visible(model);
state.set_tracks_total(total as i32);
state.set_tracks_search("".into());
// Apply the (persisted) group mode to the freshly loaded set.
derive_tracks(window);
}
FavData::Albums { items, total } => {
// Everything in the Albums tab is a favorite -> filled heart.
@@ -388,8 +390,8 @@ pub fn apply_favorites(window: &AppWindow, data: FavData) {
state.set_albums_total(total as i32);
state.set_albums_shown(n);
state.set_albums_search("".into());
state.set_albums_sort_by("default".into());
state.set_albums_group_mode("off".into());
// Apply the (persisted) sort + group to the freshly loaded set.
derive_albums(window);
}
FavData::Artists { items, total } => {
let cards: Vec<FavoriteArtistItem> = items
+88
View File
@@ -0,0 +1,88 @@
//! Persistence for the favorites toolbar choices (albums view / sort /
//! group + tracks group) across restarts. A small json under
//! `<data-dir>/qbz/favorites_ui.json`, mirroring the genre_filter store.
//! Search queries are transient and deliberately not persisted.
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use slint::ComponentHandle;
use crate::{AppWindow, FavoritesState};
#[derive(Serialize, Deserialize)]
struct Prefs {
#[serde(default = "d_grid")]
albums_view: String,
#[serde(default = "d_default")]
albums_sort: String,
#[serde(default = "d_off")]
albums_group: String,
#[serde(default = "d_off")]
tracks_group: String,
}
impl Default for Prefs {
fn default() -> Self {
Self {
albums_view: d_grid(),
albums_sort: d_default(),
albums_group: d_off(),
tracks_group: d_off(),
}
}
}
fn d_grid() -> String {
"grid".to_string()
}
fn d_default() -> String {
"default".to_string()
}
fn d_off() -> String {
"off".to_string()
}
fn store_path() -> Option<PathBuf> {
Some(dirs::data_dir()?.join("qbz").join("favorites_ui.json"))
}
fn read() -> Prefs {
let Some(path) = store_path() else {
return Prefs::default();
};
match std::fs::read(&path) {
Ok(bytes) => serde_json::from_slice(&bytes).unwrap_or_default(),
Err(_) => Prefs::default(),
}
}
/// Apply the persisted toolbar choices to `FavoritesState`. UI thread.
pub fn load(window: &AppWindow) {
let p = read();
let st = window.global::<FavoritesState>();
st.set_albums_view_mode(p.albums_view.into());
st.set_albums_sort_by(p.albums_sort.into());
st.set_albums_group_mode(p.albums_group.into());
st.set_tracks_group_mode(p.tracks_group.into());
}
/// Persist the current toolbar choices read from `FavoritesState`.
pub fn save(window: &AppWindow) {
let Some(path) = store_path() else {
return;
};
let st = window.global::<FavoritesState>();
let p = Prefs {
albums_view: st.get_albums_view_mode().into(),
albums_sort: st.get_albums_sort_by().into(),
albums_group: st.get_albums_group_mode().into(),
tracks_group: st.get_tracks_group_mode().into(),
};
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Ok(json) = serde_json::to_vec_pretty(&p) {
let _ = std::fs::write(&path, json);
}
}
+19
View File
@@ -27,6 +27,7 @@ mod discover_browse;
mod discovery_dismiss;
mod fav_cache;
mod favorites;
mod favorites_prefs;
mod foryou;
mod genre_filter;
mod home;
@@ -784,6 +785,9 @@ fn navigate_favorites(
// toolbar genre button shows the favorites selection count.
genre_filter::set_context("favorites");
genre_filter::apply_state(&w);
// Restore persisted toolbar choices before the data applies +
// derives, so the loaded view honors them.
favorites_prefs::load(&w);
});
match favorites::load_favorites(&runtime, tab).await {
Ok(data) => {
@@ -4346,6 +4350,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
if let Some(w) = weak.upgrade() {
w.global::<FavoritesState>().set_albums_sort_by(s);
favorites::derive_albums(&w);
favorites_prefs::save(&w);
}
});
}
{
// Albums grid/list view toggle (persisted).
let weak = window.as_weak();
window
.global::<FavoritesActions>()
.on_albums_set_view(move |v| {
if let Some(w) = weak.upgrade() {
w.global::<FavoritesState>().set_albums_view_mode(v);
favorites_prefs::save(&w);
}
});
}
@@ -4358,6 +4375,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
if let Some(w) = weak.upgrade() {
w.global::<FavoritesState>().set_albums_group_mode(g);
favorites::derive_albums(&w);
favorites_prefs::save(&w);
}
});
}
@@ -4480,6 +4498,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
if let Some(w) = weak.upgrade() {
w.global::<FavoritesState>().set_tracks_group_mode(g);
favorites::derive_tracks(&w);
favorites_prefs::save(&w);
}
});
}
@@ -681,14 +681,14 @@ export component FavoritesView inherits Rectangle {
icon: @image-url("../assets/icons/layout-grid.svg");
active: FavoritesState.albums-view-mode == "grid";
clicked => {
FavoritesState.albums-view-mode = "grid";
FavoritesActions.albums-set-view("grid");
}
}
ToggleButton {
icon: @image-url("../assets/icons/list.svg");
active: FavoritesState.albums-view-mode == "list";
clicked => {
FavoritesState.albums-view-mode = "list";
FavoritesActions.albums-set-view("list");
}
}
}
+1
View File
@@ -1502,6 +1502,7 @@ export global FavoritesActions {
callback bulk-action(string /* select-all | remove-selected | clear */);
callback album-action(string /* album id */, string /* action */);
callback albums-search(string /* query */);
callback albums-set-view(string /* grid | list */);
callback albums-set-sort(string /* sort key */);
callback albums-set-group(string /* off | alpha | artist */);
callback albums-shuffle();