feat(slint): favorites albums search + sort + shuffle
Add an expandable search (title / artist), a sort QbzSelect (Default / Title A-Z / Title Z-A / Artist A-Z) and a Shuffle header action (play a random visible album) to the Albums tab. albums-visible derives from the full albums set via derive_albums (shared model on the no-filter/default fast path, filtered+sorted clone otherwise — the label.rs::derive_releases pattern), and the count reflects the filter. cc
This commit is contained in:
4 files changed
+152
-3
No files matched your search
@@ -350,8 +350,15 @@ pub fn apply_favorites(window: &AppWindow, data: FavData) {
|
||||
}
|
||||
FavData::Albums { items, total } => {
|
||||
let cards: Vec<AlbumCardItem> = items.into_iter().map(to_item).collect();
|
||||
state.set_albums(ModelRc::new(VecModel::from(cards)));
|
||||
// `albums` is the full set (artwork target); `albums-visible`
|
||||
// (what the grid/list renders) shares it until a search/sort
|
||||
// forks it, so artwork stays live.
|
||||
let model = ModelRc::new(VecModel::from(cards));
|
||||
state.set_albums(model.clone());
|
||||
state.set_albums_visible(model);
|
||||
state.set_albums_total(total as i32);
|
||||
state.set_albums_search("".into());
|
||||
state.set_albums_sort_by("default".into());
|
||||
}
|
||||
FavData::Artists { items, total } => {
|
||||
let cards: Vec<FavoriteArtistItem> = items
|
||||
@@ -447,6 +454,47 @@ pub fn shuffled_tracks() -> Vec<Track> {
|
||||
tracks
|
||||
}
|
||||
|
||||
/// Re-derive the rendered Albums list (`albums-visible`) from the full
|
||||
/// `albums` set + the search query and sort key. Empty query + default
|
||||
/// order shares the full model so artwork stays live; otherwise forks a
|
||||
/// filtered + sorted clone (mirrors label.rs::derive_releases).
|
||||
pub fn derive_albums(window: &AppWindow) {
|
||||
let state = window.global::<FavoritesState>();
|
||||
let query_owned = state.get_albums_search().to_lowercase();
|
||||
let query = query_owned.trim();
|
||||
let sort = state.get_albums_sort_by().to_string();
|
||||
let all = state.get_albums();
|
||||
if query.is_empty() && sort == "default" {
|
||||
state.set_albums_visible(all);
|
||||
return;
|
||||
}
|
||||
let mut filtered: Vec<AlbumCardItem> = (0..all.row_count())
|
||||
.filter_map(|i| all.row_data(i))
|
||||
.filter(|a| {
|
||||
query.is_empty()
|
||||
|| a.title.to_lowercase().contains(query)
|
||||
|| a.artist.to_lowercase().contains(query)
|
||||
})
|
||||
.collect();
|
||||
album_map::sort_album_items(&mut filtered, &sort);
|
||||
state.set_albums_visible(ModelRc::new(VecModel::from(filtered)));
|
||||
}
|
||||
|
||||
/// A random album id from the currently-visible set (Shuffle / random).
|
||||
pub fn random_visible_album(window: &AppWindow) -> Option<String> {
|
||||
let model = window.global::<FavoritesState>().get_albums_visible();
|
||||
let n = model.row_count();
|
||||
if n == 0 {
|
||||
return None;
|
||||
}
|
||||
let seed = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(1);
|
||||
let idx = (seed % n as u64) as usize;
|
||||
model.row_data(idx).map(|a| a.id.to_string())
|
||||
}
|
||||
|
||||
// ---- Tracks multi-select (mirrors playlist.rs) -------------------------
|
||||
// Selection lives on TrackItem.selected in the rendered `tracks-visible`
|
||||
// model (which shares `tracks` when no search filter is active).
|
||||
|
||||
@@ -4276,6 +4276,43 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
// Local search over the loaded favorite albums (title / artist).
|
||||
let weak = window.as_weak();
|
||||
window
|
||||
.global::<FavoritesActions>()
|
||||
.on_albums_search(move |q| {
|
||||
if let Some(w) = weak.upgrade() {
|
||||
w.global::<FavoritesState>().set_albums_search(q);
|
||||
favorites::derive_albums(&w);
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
// Sort the favorite albums (default / title / artist).
|
||||
let weak = window.as_weak();
|
||||
window
|
||||
.global::<FavoritesActions>()
|
||||
.on_albums_set_sort(move |s| {
|
||||
if let Some(w) = weak.upgrade() {
|
||||
w.global::<FavoritesState>().set_albums_sort_by(s);
|
||||
favorites::derive_albums(&w);
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
// Play a random album from the visible favorites set.
|
||||
let weak = window.as_weak();
|
||||
window
|
||||
.global::<FavoritesActions>()
|
||||
.on_albums_shuffle(move || {
|
||||
if let Some(w) = weak.upgrade() {
|
||||
if let Some(id) = favorites::random_visible_album(&w) {
|
||||
w.invoke_media_action("album".into(), id.into(), "play".into());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
// Local search over the loaded favorite tracks (title / artist /
|
||||
// album), re-deriving the rendered list.
|
||||
|
||||
@@ -24,6 +24,7 @@ import { ExpandableSearch } from "../primitives/ExpandableSearch.slint";
|
||||
import { CircleAction } from "../primitives/CircleAction.slint";
|
||||
import { MultiSelectBar } from "../primitives/MultiSelectBar.slint";
|
||||
import { ToggleButton } from "../primitives/ToggleButton.slint";
|
||||
import { QbzSelect } from "../primitives/QbzSelect.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
|
||||
import { ListScrollbar } from "../primitives/ListScrollbar.slint";
|
||||
@@ -325,6 +326,20 @@ export component FavoritesView inherits Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Albums header action — Shuffle (play a random album).
|
||||
if FavoritesState.active-tab == "albums"
|
||||
&& FavoritesState.albums-visible.length > 0: HorizontalLayout {
|
||||
spacing: 8px;
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
CircleAction {
|
||||
icon: @image-url("../assets/icons/shuffle.svg");
|
||||
clicked => {
|
||||
FavoritesActions.albums-shuffle();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { height: 16px; }
|
||||
@@ -468,11 +483,48 @@ export component FavoritesView inherits Rectangle {
|
||||
alignment: center;
|
||||
horizontal-stretch: 1;
|
||||
Text {
|
||||
text: FavoritesState.albums.length + " " + @tr("albums");
|
||||
text: FavoritesState.albums-visible.length
|
||||
+ (FavoritesState.albums-search != ""
|
||||
? " / " + FavoritesState.albums.length
|
||||
: "")
|
||||
+ " " + @tr("albums");
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.legal;
|
||||
}
|
||||
}
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
ExpandableSearch {
|
||||
placeholder: @tr("Search");
|
||||
query <=> FavoritesState.albums-search;
|
||||
edited(q) => {
|
||||
FavoritesActions.albums-search(q);
|
||||
}
|
||||
}
|
||||
}
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
QbzSelect {
|
||||
menu-width: 168px;
|
||||
options: [
|
||||
@tr("Default"),
|
||||
@tr("Title A-Z"),
|
||||
@tr("Title Z-A"),
|
||||
@tr("Artist A-Z"),
|
||||
];
|
||||
current-index: FavoritesState.albums-sort-by == "default" ? 0
|
||||
: FavoritesState.albums-sort-by == "title-asc" ? 1
|
||||
: FavoritesState.albums-sort-by == "title-desc" ? 2
|
||||
: 3;
|
||||
selected(i) => {
|
||||
FavoritesActions.albums-set-sort(
|
||||
i == 0 ? "default"
|
||||
: i == 1 ? "title-asc"
|
||||
: i == 2 ? "title-desc"
|
||||
: "artist-asc");
|
||||
}
|
||||
}
|
||||
}
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
Rectangle {
|
||||
@@ -507,9 +559,15 @@ export component FavoritesView inherits Rectangle {
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.body;
|
||||
}
|
||||
if FavoritesState.albums.length > 0
|
||||
&& FavoritesState.albums-visible.length == 0: Text {
|
||||
text: @tr("No albums match your search.");
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.body;
|
||||
}
|
||||
|
||||
AlbumCollectionView {
|
||||
albums: FavoritesState.albums;
|
||||
albums: FavoritesState.albums-visible;
|
||||
view-mode: FavoritesState.albums-view-mode;
|
||||
is-grouped: false;
|
||||
show-source: false;
|
||||
|
||||
@@ -1457,7 +1457,10 @@ export global FavoritesState {
|
||||
in-out property <bool> tracks-multi-select: false;
|
||||
in property <int> tracks-selected-count: 0;
|
||||
in property <[AlbumCardItem]> albums: [];
|
||||
in property <[AlbumCardItem]> albums-visible: []; // after search + sort (what the grid/list renders)
|
||||
in-out property <string> albums-view-mode: "grid"; // "grid" | "list"
|
||||
in-out property <string> albums-search: "";
|
||||
in-out property <string> albums-sort-by: "default"; // default|title-asc|title-desc|artist-asc
|
||||
in property <[FavoriteArtistItem]> artists: [];
|
||||
in property <[FavoritePlaylistItem]> playlists: [];
|
||||
in property <[FavoriteLabelItem]> labels: [];
|
||||
@@ -1485,6 +1488,9 @@ export global FavoritesActions {
|
||||
callback toggle-multi-select();
|
||||
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-sort(string /* sort key */);
|
||||
callback albums-shuffle();
|
||||
}
|
||||
|
||||
// A Qobuz mix detail page (DailyQ / WeeklyQ / FavQ / TopQ), opened
|
||||
|
||||
Reference in new issue
Block a user