feat(slint): favorites tracks group-by (Off / Album / Artist / Name)

Add a group QbzSelect to the Tracks toolbar. derive_tracks reorders the
visible rows by the group key (album+title, artist+album+title, or
title) so a group's tracks sit together. Visible group-header rows are
deferred until the list is virtualized (the list is a plain for today);
this is the group-ordering half, matching how Tauri's tracks tab uses
grouping as the ordering mechanism.

cc
This commit is contained in:
vicrodh committed 2026-05-27 07:13:07 -06:00
1 parent 39b7b6d2f1
commit 2fce47bd06
4 files changed
+60 -3

No files matched your search

+23 -3
View File
@@ -427,19 +427,39 @@ pub fn derive_tracks(window: &AppWindow) {
let state = window.global::<FavoritesState>();
let query_owned = state.get_tracks_search().to_lowercase();
let query = query_owned.trim();
let group = state.get_tracks_group_mode().to_string();
let all = state.get_tracks();
if query.is_empty() {
// Fast path: no search + no grouping -> share full model (artwork live).
if query.is_empty() && group == "off" {
state.set_tracks_visible(all);
return;
}
let filtered: Vec<TrackItem> = (0..all.row_count())
let mut filtered: Vec<TrackItem> = (0..all.row_count())
.filter_map(|i| all.row_data(i))
.filter(|t| {
t.title.to_lowercase().contains(query)
query.is_empty()
|| t.title.to_lowercase().contains(query)
|| t.artist.to_lowercase().contains(query)
|| t.album.to_lowercase().contains(query)
})
.collect();
// Group-by reorders the rows so a group's tracks sit together (Tauri
// adds visible headers; v1 here is group-ordering without header rows
// until the list is virtualized).
let lc = |s: &slint::SharedString| s.to_lowercase();
match group.as_str() {
"album" => {
filtered.sort_by(|a, b| lc(&a.album).cmp(&lc(&b.album)).then(lc(&a.title).cmp(&lc(&b.title))))
}
"artist" => filtered.sort_by(|a, b| {
lc(&a.artist)
.cmp(&lc(&b.artist))
.then(lc(&a.album).cmp(&lc(&b.album)))
.then(lc(&a.title).cmp(&lc(&b.title)))
}),
"name" => filtered.sort_by(|a, b| lc(&a.title).cmp(&lc(&b.title))),
_ => {}
}
state.set_tracks_visible(ModelRc::new(VecModel::from(filtered)));
}
+12
View File
@@ -4398,6 +4398,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
});
}
{
// Group the favorite tracks (off / album / artist / name).
let weak = window.as_weak();
window
.global::<FavoritesActions>()
.on_tracks_set_group(move |g| {
if let Some(w) = weak.upgrade() {
w.global::<FavoritesState>().set_tracks_group_mode(g);
favorites::derive_tracks(&w);
}
});
}
{
// Play all favorite tracks as a fresh queue.
let runtime = app_runtime.clone();
@@ -420,6 +420,29 @@ export component FavoritesView inherits Rectangle {
}
}
}
VerticalLayout {
alignment: center;
QbzSelect {
menu-width: 150px;
options: [
@tr("Group: Off"),
@tr("Group: Album"),
@tr("Group: Artist"),
@tr("Group: Name"),
];
current-index: FavoritesState.tracks-group-mode == "off" ? 0
: FavoritesState.tracks-group-mode == "album" ? 1
: FavoritesState.tracks-group-mode == "artist" ? 2
: 3;
selected(i) => {
FavoritesActions.tracks-set-group(
i == 0 ? "off"
: i == 1 ? "album"
: i == 2 ? "artist"
: "name");
}
}
}
}
if FavoritesState.tracks.length > 0: Rectangle { height: 12px; }
+2
View File
@@ -1458,6 +1458,7 @@ export global FavoritesState {
in property <[TrackItem]> tracks: [];
in property <[TrackItem]> tracks-visible: [];
in-out property <string> tracks-search: "";
in-out property <string> tracks-group-mode: "off"; // off|album|artist|name
in-out property <bool> tracks-multi-select: false;
in property <int> tracks-selected-count: 0;
in property <[AlbumCardItem]> albums: [];
@@ -1490,6 +1491,7 @@ export global FavoritesActions {
callback play-track(string /* track id */);
callback track-action(string /* track id */, string /* action */);
callback search-tracks(string /* query */);
callback tracks-set-group(string /* off | album | artist | name */);
callback play-all-tracks();
callback shuffle-tracks();
callback toggle-multi-select();