fix(slint): spinner on home/album/artist + dedupe only in All-tab carousel
Home, Album and Artist views now show the rotating LoadingSpinner (same as search) above a label while the initial load runs, replacing the bare Loading… text. The top-result-artist dedupe moved from map_search_all into apply_search and replace_category: the full artists list is kept in SearchState.artists (used by the Artists tab grid), and a separate artists-carousel drops the duplicate next to the Most-popular hero on the All tab. cc
This commit is contained in:
6 files changed
+92
-33
No files matched your search
@@ -268,20 +268,17 @@ pub fn map_search_all(
|
||||
results: SearchAllResults,
|
||||
favorite_artists: &HashSet<u64>,
|
||||
) -> SearchData {
|
||||
let mut artists: Vec<ArtistRow> = results
|
||||
let artists: Vec<ArtistRow> = results
|
||||
.artists
|
||||
.items
|
||||
.iter()
|
||||
.map(|a| map_artist(a, favorite_artists.contains(&a.id)))
|
||||
.collect();
|
||||
let most_popular = map_most_popular(results.most_popular, favorite_artists);
|
||||
// When the top-result is an artist and is also the first entry in the
|
||||
// artists list, drop the duplicate so the carousel does not repeat it.
|
||||
if let MostPopularRow::Artist(mp) = &most_popular {
|
||||
if artists.first().map_or(false, |a| a.id == mp.id) {
|
||||
artists.remove(0);
|
||||
}
|
||||
}
|
||||
// Dedupe used to drop the top-result artist from the artists list
|
||||
// here, but the Artists tab does not show the Most-popular hero —
|
||||
// it should keep the artist. The dedupe now lives at `apply_search`
|
||||
// where the carousel-only `artists_carousel` is built.
|
||||
SearchData {
|
||||
query: query.to_string(),
|
||||
albums_total: results.albums.total,
|
||||
@@ -394,10 +391,23 @@ pub fn apply_search(window: &AppWindow, data: SearchData) {
|
||||
let artists: Vec<SlimItem> = data.artists.into_iter().map(artist_item).collect();
|
||||
let playlists: Vec<SearchPlaylistItem> =
|
||||
data.playlists.into_iter().map(playlist_item).collect();
|
||||
// Carousel variant of the artists list — drops the first entry when
|
||||
// it equals the most-popular hero, so the All tab does not duplicate
|
||||
// the Top result alongside the carousel.
|
||||
let mp_id = if let MostPopularRow::Artist(ref mp) = data.most_popular {
|
||||
Some(mp.id.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let artists_carousel: Vec<SlimItem> = match (mp_id, artists.first()) {
|
||||
(Some(id), Some(first)) if first.id == id.as_str() => artists[1..].to_vec(),
|
||||
_ => artists.clone(),
|
||||
};
|
||||
|
||||
state.set_albums(ModelRc::new(VecModel::from(albums)));
|
||||
state.set_tracks(ModelRc::new(VecModel::from(tracks)));
|
||||
state.set_artists(ModelRc::new(VecModel::from(artists)));
|
||||
state.set_artists_carousel(ModelRc::new(VecModel::from(artists_carousel)));
|
||||
state.set_playlists(ModelRc::new(VecModel::from(playlists)));
|
||||
|
||||
state.set_albums_total(data.albums_total as i32);
|
||||
@@ -639,18 +649,22 @@ pub fn replace_category(window: &AppWindow, more: MoreRows) {
|
||||
let items: Vec<SearchTrackItem> = rows.into_iter().map(track_item).collect();
|
||||
state.set_tracks(ModelRc::new(VecModel::from(items)));
|
||||
}
|
||||
MoreRows::Artists(mut rows) => {
|
||||
// Re-apply the top-result dedupe — if the most-popular hero
|
||||
// is an artist and is the first entry in the new list, drop
|
||||
// it so the carousel does not show the duplicate.
|
||||
if state.get_most_popular_kind().as_str() == "artist" {
|
||||
let mp_id = state.get_most_popular_artist().id;
|
||||
if rows.first().map_or(false, |r| r.id == mp_id.as_str()) {
|
||||
rows.remove(0);
|
||||
}
|
||||
}
|
||||
MoreRows::Artists(rows) => {
|
||||
// Rebuild both lists: the Artists tab keeps every result; the
|
||||
// All-tab carousel drops the duplicate next to the Most-popular
|
||||
// hero.
|
||||
let items: Vec<SlimItem> = rows.into_iter().map(artist_item).collect();
|
||||
let mp_id = if state.get_most_popular_kind().as_str() == "artist" {
|
||||
Some(state.get_most_popular_artist().id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let carousel: Vec<SlimItem> = match (mp_id, items.first()) {
|
||||
(Some(id), Some(first)) if first.id == id.as_str() => items[1..].to_vec(),
|
||||
_ => items.clone(),
|
||||
};
|
||||
state.set_artists(ModelRc::new(VecModel::from(items)));
|
||||
state.set_artists_carousel(ModelRc::new(VecModel::from(carousel)));
|
||||
}
|
||||
MoreRows::Playlists(rows) => {
|
||||
let items: Vec<SearchPlaylistItem> = rows.into_iter().map(playlist_item).collect();
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ContextMenuItem } from "../primitives/ContextMenuItem.slint";
|
||||
import { QualityBadgeFull } from "../primitives/QualityBadgeFull.slint";
|
||||
import { TrackPlayCell } from "../primitives/TrackPlayCell.slint";
|
||||
import { TrackContextMenu } from "../primitives/TrackContextMenu.slint";
|
||||
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
|
||||
import { NavButtons } from "../shell/NavButtons.slint";
|
||||
|
||||
component CircleAction inherits Rectangle {
|
||||
@@ -528,10 +529,22 @@ export component AlbumPageView inherits Rectangle {
|
||||
spacing: 0px;
|
||||
alignment: start;
|
||||
|
||||
if AlbumState.loading && AlbumState.tracks.length == 0: Text {
|
||||
text: @tr("Loading album...");
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.body;
|
||||
if AlbumState.loading && AlbumState.tracks.length == 0: Rectangle {
|
||||
height: 280px;
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
spacing: 18px;
|
||||
HorizontalLayout {
|
||||
alignment: center;
|
||||
LoadingSpinner { size: 36px; }
|
||||
}
|
||||
Text {
|
||||
text: @tr("Loading album…");
|
||||
color: Theme.text-muted;
|
||||
font-size: 13px;
|
||||
horizontal-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Toolbar — album quality badge and track search.
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ContextMenu } from "../primitives/ContextMenu.slint";
|
||||
import { ContextMenuItem } from "../primitives/ContextMenuItem.slint";
|
||||
import { TrackPlayCell } from "../primitives/TrackPlayCell.slint";
|
||||
import { TrackContextMenu } from "../primitives/TrackContextMenu.slint";
|
||||
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
|
||||
import { Carousel } from "../discover/Carousel.slint";
|
||||
import { NavButtons } from "../shell/NavButtons.slint";
|
||||
|
||||
@@ -297,10 +298,22 @@ export component ArtistPageView inherits Rectangle {
|
||||
|
||||
Rectangle { height: 36px; }
|
||||
|
||||
if ArtistState.loading && ArtistState.top-tracks.length == 0: Text {
|
||||
text: @tr("Loading artist...");
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.body;
|
||||
if ArtistState.loading && ArtistState.top-tracks.length == 0: Rectangle {
|
||||
height: 280px;
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
spacing: 18px;
|
||||
HorizontalLayout {
|
||||
alignment: center;
|
||||
LoadingSpinner { size: 36px; }
|
||||
}
|
||||
Text {
|
||||
text: @tr("Loading artist…");
|
||||
color: Theme.text-muted;
|
||||
font-size: 13px;
|
||||
horizontal-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Popular tracks ----------------------------------------------
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Carousel } from "Carousel.slint";
|
||||
import { GridSection } from "GridSection.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { NavButtons } from "../shell/NavButtons.slint";
|
||||
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
|
||||
|
||||
// Header + hint shown for a recently-played module while it has no data
|
||||
// yet (the play history fills once the playback session records plays).
|
||||
@@ -217,10 +218,22 @@ export component HomeView inherits Rectangle {
|
||||
spacing: 40px;
|
||||
alignment: start;
|
||||
|
||||
if HomeState.loading && HomeState.sections.length == 0: Text {
|
||||
text: @tr("Loading your music...");
|
||||
color: Theme.text-muted;
|
||||
font-size: 14px;
|
||||
if HomeState.loading && HomeState.sections.length == 0: Rectangle {
|
||||
height: 320px;
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
spacing: 18px;
|
||||
HorizontalLayout {
|
||||
alignment: center;
|
||||
LoadingSpinner { size: 36px; }
|
||||
}
|
||||
Text {
|
||||
text: @tr("Loading your music…");
|
||||
color: Theme.text-muted;
|
||||
font-size: 13px;
|
||||
horizontal-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for section in HomeState.sections: Carousel {
|
||||
|
||||
@@ -489,11 +489,13 @@ export component SearchResultsView inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Artists carousel — fills the rest of the row.
|
||||
if SearchState.artists.length > 0: ArtistCarousel {
|
||||
// Artists carousel — fills the rest of the row. Uses
|
||||
// the carousel-only list so the Most-popular hero is
|
||||
// not duplicated next to it.
|
||||
if SearchState.artists-carousel.length > 0: ArtistCarousel {
|
||||
horizontal-stretch: 1;
|
||||
title: @tr("Artists");
|
||||
artists: SearchState.artists;
|
||||
artists: SearchState.artists-carousel;
|
||||
show-view-all: true;
|
||||
artist-clicked(id) => { root.open-artist(id); }
|
||||
media-action(k, i, a) => { root.media-action(k, i, a); }
|
||||
|
||||
@@ -372,6 +372,10 @@ export global SearchState {
|
||||
in property <[AlbumCardItem]> albums: [];
|
||||
in property <[SearchTrackItem]> tracks: [];
|
||||
in property <[SlimItem]> artists: [];
|
||||
// Carousel-only variant of artists — skips the top-result artist when
|
||||
// it is also the first list entry, so the All-tab carousel does not
|
||||
// duplicate the Most-popular hero. The Artists tab uses `artists`.
|
||||
in property <[SlimItem]> artists-carousel: [];
|
||||
in property <[SearchPlaylistItem]> playlists: [];
|
||||
in property <int> albums-total: 0;
|
||||
in property <int> tracks-total: 0;
|
||||
|
||||
Reference in new issue
Block a user