feat(discover): Most Played Albums carousel in Home + For You

Wires the rail (top-20 by play count) into both tabs, mirroring the favoriteAlbums pattern: HomeState/ForYouState.most-played-albums DiscoverSection; foryou::most_played_album_cards maps the ranked rows onto AlbumCard; home.rs builds it inline (local, no fetch) and For You rides the favorites branch; HomeMostPlayedAlbum/ForYouMostPlayedAlbum artwork targets + apply arms; render arms in HomeView/ForYouView (self-hide while empty); title in 8 locales. slintcheck green. View-all page (search + play count) is the next increment.

cc
This commit is contained in:
vicrodh committed 2026-07-14 01:06:54 -06:00
1 parent abb529e22b
commit 1a7853d9c6
16 files changed
+133 -3

No files matched your search

@@ -4519,3 +4519,7 @@ msgstr "Gibt zwischengespeicherte Daten frei. Deine heruntergeladenen Alben blei
msgid "Couldn't clear the cache"
msgstr "Cache konnte nicht geleert werden"
# Most Played Albums rail title
msgid "Most Played Albums"
msgstr "Meistgespielte Alben"
@@ -291,3 +291,7 @@ msgstr "Cache cleared"
msgid "Couldn't clear the cache"
msgstr "Couldn't clear the cache"
# Most Played Albums rail title
msgid "Most Played Albums"
msgstr "Most Played Albums"
@@ -4549,3 +4549,7 @@ msgstr "Libera datos en caché. Tus álbumes descargados se conservan — elimí
msgid "Couldn't clear the cache"
msgstr "No se pudo limpiar el caché"
# Most Played Albums rail title
msgid "Most Played Albums"
msgstr "Álbumes más escuchados"
@@ -4519,3 +4519,7 @@ msgstr "Libère les données en cache. Vos albums téléchargés sont conservés
msgid "Couldn't clear the cache"
msgstr "Impossible de vider le cache"
# Most Played Albums rail title
msgid "Most Played Albums"
msgstr "Albums les plus écoutés"
@@ -4508,3 +4508,7 @@ msgstr "キャッシュデータを解放します。ダウンロード済みの
msgid "Couldn't clear the cache"
msgstr "キャッシュを削除できませんでした"
# Most Played Albums rail title
msgid "Most Played Albums"
msgstr "最も再生したアルバム"
@@ -4531,3 +4531,7 @@ msgstr "Maakt cachegegevens vrij. Je gedownloade albums blijven behouden — ver
msgid "Couldn't clear the cache"
msgstr "Kan de cache niet wissen"
# Most Played Albums rail title
msgid "Most Played Albums"
msgstr "Meest afgespeelde albums"
@@ -4519,3 +4519,7 @@ msgstr "Libera dados em cache. Seus álbuns baixados são mantidos — remova-os
msgid "Couldn't clear the cache"
msgstr "Não foi possível limpar o cache"
# Most Played Albums rail title
msgid "Most Played Albums"
msgstr "Álbuns mais tocados"
@@ -4532,3 +4532,7 @@ msgstr "Освобождает кэшированные данные. Загру
msgid "Couldn't clear the cache"
msgstr "Не удалось очистить кэш"
# Most Played Albums rail title
msgid "Most Played Albums"
msgstr "Самые прослушиваемые альбомы"
@@ -129,6 +129,17 @@ export component ForYouView inherits VerticalLayout {
}
}
// mostPlayedAlbums — album carousel ranked by local play count.
if desc.id == "mostPlayedAlbums" && ForYouState.most-played-albums.albums.length > 0: Carousel {
section: ForYouState.most-played-albums;
album-clicked(id) => {
root.open-album(id);
}
media-action(kind, id, action) => {
root.media-action(kind, id, action);
}
}
// similarAlbums — "More From Your Library" album carousel.
if desc.id == "similarAlbums" && ForYouState.more-from-library.albums.length > 0: Carousel {
section: ForYouState.more-from-library;
+13
View File
@@ -482,6 +482,19 @@ export component HomeView inherits Rectangle {
}
}
// Most Played Albums — album carousel ranked by local play
// count. Self-hides while empty (default-off section).
if desc.id == "mostPlayedAlbums"
&& HomeState.most-played-albums.albums.length > 0: Carousel {
section: HomeState.most-played-albums;
album-clicked(id) => {
root.album-clicked(id);
}
media-action(kind, id, action) => {
root.media-action(kind, id, action);
}
}
// Qobuz Mixes (#566) — static navigation tiles, ALWAYS
// rendered when enabled (no data gate; 1:1 with the For
// You arm and Tauri Home).
+6
View File
@@ -180,6 +180,9 @@ export global HomeState {
// reorders HomeState.favorite-albums in place when this changes.
in-out property <int> library-albums-sort: 0;
in property <DiscoverSection> release-watch;
// Most Played Albums rail — top albums by local play count (title set by
// Rust from `album_play_history::top_albums`). Self-hides while empty.
in property <DiscoverSection> most-played-albums;
in property <[SlimItem]> top-artists: [];
// Qobuz Playlists row (Home + Editor's Picks). Single-cover cards; the
// active tab's set is swapped in by crate::home::select_tab.
@@ -344,6 +347,9 @@ export global ForYouState {
in property <DiscoverSection> recent-albums;
// Favorite Albums — the user's favorited albums (For You #6).
in property <DiscoverSection> favorite-albums;
// Most Played Albums — top albums by local play count (same data as
// HomeState.most-played-albums; local, not tab-specific).
in property <DiscoverSection> most-played-albums;
in property <[SlimItem]> recent-tracks: [];
in property <[SlimItem]> top-artists: [];
in property <[SlimItem]> artists-to-follow: [];
+20
View File
@@ -74,6 +74,9 @@ pub enum ArtworkTarget {
/// "Library Albums" rail (#566). Separate from `ForYouFavoriteAlbum`:
/// the two rails share the data pipeline but not the model lifecycle.
HomeFavoriteAlbum { idx: usize },
/// A card in `HomeState.most-played-albums.albums[idx]` — the Home tab's
/// "Most Played Albums" rail (local play-count ranking).
HomeMostPlayedAlbum { idx: usize },
/// A card in `HomeState.release-watch.albums[idx]` — the Home tab's
/// "Release Watch" rail (#566; ForYouReleaseWatch's Home twin).
HomeReleaseWatchAlbum { idx: usize },
@@ -218,6 +221,8 @@ pub enum ArtworkTarget {
ExtRecoWeeklyJams { index: usize },
/// A card in ForYouState.favorite-albums.albums[index].
ForYouFavoriteAlbum { index: usize },
/// A card in ForYouState.most-played-albums.albums[index].
ForYouMostPlayedAlbum { index: usize },
/// The Spotlight artist portrait.
ForYouSpotlightArtist,
/// A card in ForYouState.spotlight-albums[index].
@@ -949,6 +954,14 @@ fn apply_artwork(
item.artwork = image;
section.albums.set_row_data(idx, item);
}
ArtworkTarget::HomeMostPlayedAlbum { idx } => {
let section = home.get_most_played_albums();
let Some(mut item) = section.albums.row_data(idx) else {
return;
};
item.artwork = image;
section.albums.set_row_data(idx, item);
}
ArtworkTarget::HomeReleaseWatchAlbum { idx } => {
let section = home.get_release_watch();
let Some(mut item) = section.albums.row_data(idx) else {
@@ -1544,6 +1557,13 @@ fn apply_artwork(
model.set_row_data(index, item);
}
}
ArtworkTarget::ForYouMostPlayedAlbum { index } => {
let model = window.global::<crate::ForYouState>().get_most_played_albums().albums;
if let Some(mut item) = model.row_data(index) {
item.artwork = image;
model.set_row_data(index, item);
}
}
ArtworkTarget::ForYouSpotlightArtist => {
window
.global::<crate::ForYouState>()
+3 -3
View File
@@ -151,9 +151,8 @@ pub fn render_kind(id: DiscoverySectionId) -> &'static str {
RadioStations => "radio",
TopArtists | ArtistsToFollow => "artistCarousel",
ArtistSpotlight => "spotlight",
ReleaseWatch | FavoriteAlbums | SimilarAlbums | RediscoverLibrary | EssentialsByGenre => {
"albumCarousel"
}
ReleaseWatch | FavoriteAlbums | MostPlayedAlbums | SimilarAlbums | RediscoverLibrary
| EssentialsByGenre => "albumCarousel",
}
}
@@ -189,6 +188,7 @@ pub fn label_for(id: DiscoverySectionId) -> &'static str {
EssentialsByGenre => qbz_i18n::mark("Essentials by Genre"), // discovery.essentialsByGenre
ArtistsToFollow => qbz_i18n::mark("Artists to Follow"), // discovery.artistsToFollow
ArtistSpotlight => qbz_i18n::mark("Artist Spotlight"), // discovery.artistSpotlight
MostPlayedAlbums => qbz_i18n::mark("Most Played Albums"), // local: most-played rail
}
}
+32
View File
@@ -492,6 +492,25 @@ where
build_favorite_albums(&fav_albums)
}
/// Cards for the "Most Played Albums" rail — top 20 albums by local play
/// count (`album_play_history`). Local (no network); the SAME set feeds Home
/// and For You. Ranked rows map 1:1 onto `AlbumCard`.
pub(crate) fn most_played_album_cards() -> Vec<AlbumCard> {
qbz_app::settings::album_play_history::top_albums(20)
.into_iter()
.map(|r| AlbumCard {
id: r.album_id,
title: r.title,
artist: r.artist,
artist_id: r.artist_id,
year: r.year,
quality_tier: r.quality_tier,
quality_label: r.quality_label,
artwork_url: r.artwork_url,
})
.collect()
}
/// Radio Stations — album-seeded tiles from recent + favorite albums,
/// deduped, capped at 12.
fn build_radio(
@@ -756,6 +775,16 @@ fn apply_favorite_albums(weak: &slint::Weak<AppWindow>, cache: &ImageCache, card
crate::artwork::spawn_loads(jobs, weak.clone(), cache.clone());
}
fn apply_most_played_albums(weak: &slint::Weak<AppWindow>, cache: &ImageCache, cards: Vec<AlbumCard>) {
let jobs = album_jobs(&cards, |i| ArtworkTarget::ForYouMostPlayedAlbum { index: i });
let w = weak.clone();
let _ = w.upgrade_in_event_loop(move |w| {
w.global::<ForYouState>()
.set_most_played_albums(section(&qbz_i18n::t("Most Played Albums"), &cards));
});
crate::artwork::spawn_loads(jobs, weak.clone(), cache.clone());
}
fn apply_more_from_library(
weak: &slint::Weak<AppWindow>,
cache: &ImageCache,
@@ -933,6 +962,9 @@ pub fn spawn_for_you<A>(
let scored_fav = crate::reco::scored_favorite_album_ids(80);
let fav_albums = order_by_score(fav_albums, scored_fav.as_deref());
apply_favorite_albums(&weak, &cache, build_favorite_albums(&fav_albums));
// Most Played Albums — local play-count rail (no fetch); rides
// this branch since it shares the cache + weak already cloned.
apply_most_played_albums(&weak, &cache, most_played_album_cards());
// reco: backfill genres for the resolved favorite albums so the
// engine's top-genres has data (plays alone carry no genre).
let genre_entries: Vec<(String, u64, String)> = fav_albums
+10
View File
@@ -52,6 +52,10 @@ pub struct HomeData {
/// fourth ported Tauri-Home section, is static navigation tiles — no
/// data field needed.)
pub top_artists: Vec<crate::foryou::ArtistSlim>,
/// "Most Played Albums" rail — top albums by local play count
/// (`album_play_history`). Local, so built inline (no fetch). Feeds
/// `HomeState.most-played-albums`; self-hides while empty.
pub most_played_albums: Vec<crate::foryou::AlbumCard>,
}
thread_local! {
@@ -319,6 +323,7 @@ where
favorite_albums,
release_watch,
top_artists,
most_played_albums: crate::foryou::most_played_album_cards(),
})
}
@@ -778,6 +783,7 @@ const HOME_RENDERABLE: &[DiscoverySectionId] = &[
DiscoverySectionId::QobuzMixes,
DiscoverySectionId::ReleaseWatch,
DiscoverySectionId::TopArtists,
DiscoverySectionId::MostPlayedAlbums,
];
/// Build one tab's ordered ENABLED descriptor list from `prefs` + the cached
@@ -1097,6 +1103,10 @@ pub fn apply_home(window: &AppWindow, data: HomeData) {
&qbz_i18n::t("Release Watch"),
&data.release_watch,
));
state.set_most_played_albums(crate::foryou::section(
&qbz_i18n::t("Most Played Albums"),
&data.most_played_albums,
));
state.set_top_artists(ModelRc::new(VecModel::from(crate::foryou::artist_items(
&data.top_artists,
))));
+6
View File
@@ -753,6 +753,12 @@ async fn reload_home(
url: card.artwork_url.clone(),
})
}));
jobs.extend(data.most_played_albums.iter().enumerate().filter_map(|(idx, card)| {
(!card.artwork_url.is_empty()).then(|| artwork::ArtworkJob {
target: artwork::ArtworkTarget::HomeMostPlayedAlbum { idx },
url: card.artwork_url.clone(),
})
}));
jobs.extend(data.release_watch.iter().enumerate().filter_map(|(idx, card)| {
(!card.artwork_url.is_empty()).then(|| artwork::ArtworkJob {
target: artwork::ArtworkTarget::HomeReleaseWatchAlbum { idx },