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:
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;
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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: [];
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
))));
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in new issue
Block a user