feat(home): sort control for the Library Albums rail

Adds a compact dropdown in the Library Albums (favoriteAlbums) row header, left of the page chevrons: Recently added (default), First added (reverse), Random (shuffle). Reorders the cached base list in Rust with no re-fetch and re-fires artwork by the new index. Also corrects the stale HomeView comment that claimed the recently-played refresh lives in the toolbar (it is a row-header button). 8 locales updated.

cc
This commit is contained in:
vicrodh committed 2026-07-13 21:01:48 -06:00
1 parent dfc74fb6d4
commit 41f8578b78
13 files changed
+202 -5

No files matched your search

@@ -4500,3 +4500,14 @@ msgstr "Ausgabe: {}"
# Local Library header Plex sync button (#573)
msgid "Sync Plex library"
msgstr "Plex-Bibliothek synchronisieren"
# Library Albums sort options (#566)
msgid "Recently added"
msgstr "Zuletzt hinzugefügt"
msgid "First added"
msgstr "Zuerst hinzugefügt"
msgid "Random"
msgstr "Zufällig"
@@ -269,3 +269,14 @@ msgstr "Output: {}"
# Local Library header Plex sync button (#573)
msgid "Sync Plex library"
msgstr "Sync Plex library"
# Library Albums sort options (#566)
msgid "Recently added"
msgstr "Recently added"
msgid "First added"
msgstr "First added"
msgid "Random"
msgstr "Random"
@@ -4530,3 +4530,14 @@ msgstr "Salida: {}"
# Local Library header Plex sync button (#573)
msgid "Sync Plex library"
msgstr "Sincronizar biblioteca de Plex"
# Library Albums sort options (#566)
msgid "Recently added"
msgstr "Recién agregados"
msgid "First added"
msgstr "Primeros agregados"
msgid "Random"
msgstr "Aleatorio"
@@ -4500,3 +4500,14 @@ msgstr "Sortie : {}"
# Local Library header Plex sync button (#573)
msgid "Sync Plex library"
msgstr "Synchroniser la bibliothèque Plex"
# Library Albums sort options (#566)
msgid "Recently added"
msgstr "Ajoutés récemment"
msgid "First added"
msgstr "Ajoutés en premier"
msgid "Random"
msgstr "Aléatoire"
@@ -4489,3 +4489,14 @@ msgstr "出力: {}"
# Local Library header Plex sync button (#573)
msgid "Sync Plex library"
msgstr "Plexライブラリを同期"
# Library Albums sort options (#566)
msgid "Recently added"
msgstr "最近追加した順"
msgid "First added"
msgstr "追加が古い順"
msgid "Random"
msgstr "ランダム"
@@ -4512,3 +4512,14 @@ msgstr "Uitvoer: {}"
# Local Library header Plex sync button (#573)
msgid "Sync Plex library"
msgstr "Plex-bibliotheek synchroniseren"
# Library Albums sort options (#566)
msgid "Recently added"
msgstr "Recent toegevoegd"
msgid "First added"
msgstr "Eerst toegevoegd"
msgid "Random"
msgstr "Willekeurig"
@@ -4500,3 +4500,14 @@ msgstr "Saída: {}"
# Local Library header Plex sync button (#573)
msgid "Sync Plex library"
msgstr "Sincronizar biblioteca do Plex"
# Library Albums sort options (#566)
msgid "Recently added"
msgstr "Adicionados recentemente"
msgid "First added"
msgstr "Adicionados primeiro"
msgid "Random"
msgstr "Aleatório"
@@ -4513,3 +4513,14 @@ msgstr "Вывод: {}"
# Local Library header Plex sync button (#573)
msgid "Sync Plex library"
msgstr "Синхронизировать библиотеку Plex"
# Library Albums sort options (#566)
msgid "Recently added"
msgstr "Недавно добавленные"
msgid "First added"
msgstr "Добавленные первыми"
msgid "Random"
msgstr "Случайно"
+24
View File
@@ -10,6 +10,7 @@ import { Typography } from "../foundation/typography.slint";
import { DiscoverSection } from "../state.slint";
import { AlbumCard } from "AlbumCard.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { QbzSelect } from "../primitives/QbzSelect.slint";
component NavButton inherits Rectangle {
in property <image> icon;
@@ -71,6 +72,14 @@ export component Carousel inherits VerticalLayout {
in property <bool> show-view-all: false;
// Optional refresh affordance in the row header (recently-played rows).
in property <bool> show-refresh: false;
// Optional sort dropdown in the row header, above/left of the page
// chevrons (Library Albums #566). `sort-options` are the localized
// labels; `sort-index` is the current selection; `sort-changed(i)` fires
// when the user picks another option.
in property <bool> show-sort: false;
in property <[string]> sort-options: [];
in property <int> sort-index: 0;
callback sort-changed(int);
callback album-clicked(string);
callback media-action(string, string, string);
callback view-all-clicked();
@@ -182,6 +191,21 @@ export component Carousel inherits VerticalLayout {
}
HorizontalLayout {
spacing: 4px;
// Sort dropdown (Library Albums): sits left of the page chevrons,
// a compact toolbar-style select. Picking an option fires
// sort-changed(i); the host re-orders the row's data in Rust.
if root.show-sort: VerticalLayout {
alignment: center;
QbzSelect {
sm: true;
menu-width: 170px;
options: root.sort-options;
current-index: root.sort-index;
selected(i) => {
root.sort-changed(i);
}
}
}
// "View all" sits to the LEFT of the carousel page chevrons,
// matching the Tauri DiscoverySection header.
if root.show-view-all: VerticalLayout {
+16 -3
View File
@@ -121,9 +121,10 @@ component GenreButton inherits Rectangle {
}
}
// Manual refresh for the recently-played rails — sits in the toolbar next
// to the nav cluster. Same 32px icon-button form as GearButton, plus the
// global hover tooltip (TooltipState — the TransportControls pattern).
// Settings/config gear for the Home configurator, anchored in the toolbar's
// right controls. (The recently-played refresh is NOT here — it lives in each
// recently-played row header, left of the page chevrons; see Carousel
// `show-refresh`.)
component GearButton inherits Rectangle {
callback clicked();
@@ -461,6 +462,18 @@ export component HomeView inherits Rectangle {
if desc.id == "favoriteAlbums"
&& HomeState.favorite-albums.albums.length > 0: Carousel {
section: HomeState.favorite-albums;
// Sort dropdown left of the page chevrons (#566).
show-sort: true;
sort-options: [
@tr("Recently added"),
@tr("First added"),
@tr("Random"),
];
sort-index: HomeState.library-albums-sort;
sort-changed(i) => {
HomeState.library-albums-sort = i;
HomeActions.set-library-albums-sort(i);
}
album-clicked(id) => {
root.album-clicked(id);
}
+9 -2
View File
@@ -175,6 +175,10 @@ export global HomeState {
// top-artists' title is @tr in the view arm (bare list, like For You).
// (qobuzMixes, the fourth ported section, is static tiles — no state.)
in property <DiscoverSection> favorite-albums;
// Library Albums (favoriteAlbums) sort order: 0 = Recently added
// (default), 1 = First added (reverse), 2 = Random (shuffled). Rust
// reorders HomeState.favorite-albums in place when this changes.
in-out property <int> library-albums-sort: 0;
in property <DiscoverSection> release-watch;
in property <[SlimItem]> top-artists: [];
// Qobuz Playlists row (Home + Editor's Picks). Single-cover cards; the
@@ -209,9 +213,12 @@ export global HomeActions {
// Home was off-screen (dirty-flag gated in Rust — a no-op otherwise, so
// mounting Home stays free).
callback home-mounted();
// Manual refresh (the toolbar button next to the nav cluster):
// unconditionally re-read the local recently-played rails.
// Manual refresh (the row-header button next to the Recently Played
// Albums nav chevrons): unconditionally re-read the local rails.
callback refresh-recent();
// Library Albums sort changed (see HomeState.library-albums-sort). Rust
// reorders the cached base list and re-pushes favorite-albums + artwork.
callback set-library-albums-sort(int);
}
// RecentAlbumsView — the full "Recently Played Albums" page opened from the
+5
View File
@@ -1084,6 +1084,11 @@ pub fn apply_home(window: &AppWindow, data: HomeData) {
// For You twins (foryou::apply_favorite_albums / apply_release_watch /
// apply_top_artists), separate lifecycles. Top Artists' title lives in
// the HomeView arm (@tr, like ForYouView's) — its model is a bare list.
// Cache the base (Recently-added) order so the header sort dropdown can
// reorder without a re-fetch, and reset the selection to the default on
// every fresh load (the load-time artwork dispatch is in base order).
*crate::LIB_ALBUMS_BASE.lock().unwrap() = data.favorite_albums.clone();
state.set_library_albums_sort(0);
state.set_favorite_albums(crate::foryou::section(
&qbz_i18n::t("Library Albums"),
&data.favorite_albums,
+60
View File
@@ -3004,6 +3004,57 @@ fn refresh_recent_rails(
});
}
/// Base ("Recently added") order of the Home "Library Albums" rail
/// (`favoriteAlbums`, #566), cached on every home load so the header sort
/// dropdown can reorder without a re-fetch. Read by `apply_library_albums_sort`.
pub(crate) static LIB_ALBUMS_BASE: std::sync::Mutex<Vec<crate::foryou::AlbumCard>> =
std::sync::Mutex::new(Vec::new());
/// Reorder the cached Library-Albums base list per the sort selection.
/// `mode`: 0 = Recently added (base), 1 = First added (reverse), 2 = Random.
fn library_albums_sorted(mode: i32) -> Vec<crate::foryou::AlbumCard> {
let mut cards = LIB_ALBUMS_BASE.lock().unwrap().clone();
match mode {
1 => cards.reverse(),
2 => {
use rand::seq::SliceRandom;
cards.shuffle(&mut rand::rng());
}
_ => {}
}
cards
}
/// Re-push the Home "Library Albums" rail in the chosen sort order + re-fire
/// its artwork by the new index. No re-fetch — reorders the cached base list
/// (mirrors `refresh_recent_rails`' local re-apply). Runs on the UI thread
/// (Slint callback); `spawn_loads` does its own threading and the covers are
/// cache-served, so a reorder is near-instant.
fn apply_library_albums_sort(
weak: slint::Weak<AppWindow>,
mode: i32,
image_cache: artwork::ImageCache,
) {
let cards = library_albums_sorted(mode);
if let Some(w) = weak.upgrade() {
w.global::<HomeState>().set_favorite_albums(crate::foryou::section(
&qbz_i18n::t("Library Albums"),
&cards,
));
}
let jobs: Vec<artwork::ArtworkJob> = cards
.iter()
.enumerate()
.filter_map(|(idx, card)| {
(!card.artwork_url.is_empty()).then(|| artwork::ArtworkJob {
target: artwork::ArtworkTarget::HomeFavoriteAlbum { idx },
url: card.artwork_url.clone(),
})
})
.collect();
artwork::spawn_loads(jobs, weak, image_cache);
}
/// Playback hook: a play was just recorded in the recently-played store
/// (`playback::record_recent`). Marks the Home rails stale and — when the
/// Home view is currently showing — refreshes them immediately, so the rails
@@ -15222,6 +15273,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
refresh_recent_rails(weak.clone(), &handle, image_cache.clone());
});
}
// Library Albums (#566) header sort: reorder the cached favorite-albums
// base list (0 recent / 1 first / 2 random) and re-push it + its covers.
{
let weak = window.as_weak();
let image_cache = image_cache.clone();
window.global::<HomeActions>().on_set_library_albums_sort(move |mode| {
apply_library_albums_sort(weak.clone(), mode, image_cache.clone());
});
}
// Qobuz Playlists category filter (multi-select, client-side). Toggling /
// clearing a tag re-filters the cached playlists row and re-fires the