feat(slint): carry the dropped favorites-label image through the data path

The favorites-labels wire item (LegacyLabelDto) carries an `image`
field that FavLabel silently dropped, so label cards had no artwork.
Parse it (bare string or the object form, via label::extract_label_image),
plumb it onto LabelCard + FavoriteLabelItem, and issue a FavoriteLabel
artwork job + apply arm mirroring favorite artists. Align the count line
to "{n} albums" (matches Tauri and the sibling artist card) and seed
labels-visible for the upcoming search filter.

cc
This commit is contained in:
vicrodh committed 2026-05-27 13:09:27 -06:00
1 parent 4cdd015209
commit 095be4ff57
4 files changed
+45 -9

No files matched your search

+9
View File
@@ -82,6 +82,8 @@ pub enum ArtworkTarget {
FavoriteArtist { index: usize },
/// A card in FavoritesState.playlists[index].
FavoritePlaylist { index: usize },
/// A card in FavoritesState.labels[index].
FavoriteLabel { index: usize },
/// A card in ForYouState.release-watch.albums[index].
ForYouReleaseWatch { index: usize },
/// A card in ForYouState.recent-albums.albums[index].
@@ -541,6 +543,13 @@ fn apply_artwork(
model.set_row_data(index, item);
}
}
ArtworkTarget::FavoriteLabel { index } => {
let model = window.global::<crate::FavoritesState>().get_labels();
if let Some(mut item) = model.row_data(index) {
item.image = image;
model.set_row_data(index, item);
}
}
ArtworkTarget::ForYouReleaseWatch { index } => {
let model = window.global::<crate::ForYouState>().get_release_watch().albums;
if let Some(mut item) = model.row_data(index) {
+28 -5
View File
@@ -85,7 +85,10 @@ impl FavTab {
/// Favorites-labels response item — the qbz-models `Label` is just
/// {id, name}, but the favorites payload carries an image + count,
/// so parse into this richer local shape.
/// so parse into this richer local shape. `image` is a bare string on
/// the wire (LegacyLabelDto), but typed as Value to also tolerate the
/// `{mega|extralarge|large|thumbnail|small}` object form other label
/// surfaces return (resolved via `label::extract_label_image`).
#[derive(Deserialize)]
struct FavLabel {
#[serde(default)]
@@ -94,6 +97,8 @@ struct FavLabel {
name: String,
#[serde(default)]
albums_count: Option<u32>,
#[serde(default)]
image: Option<serde_json::Value>,
}
pub enum FavData {
@@ -141,6 +146,7 @@ pub struct LabelCard {
pub id: String,
pub name: String,
pub albums_line: String,
pub image_url: String,
}
/// Fetch + parse one favorites tab.
@@ -378,14 +384,17 @@ fn map_playlist(playlist: Playlist) -> PlaylistCard {
}
fn map_label(label: FavLabel) -> LabelCard {
// Tauri's favorites label card says "{n} albums" (library.albumCount),
// matching the sibling FavArtistCard's "{n} albums".
let albums_line = match label.albums_count {
Some(n) if n > 0 => format!("{} releases", n),
Some(n) if n > 0 => format!("{} albums", n),
_ => String::new(),
};
LabelCard {
id: label.id.to_string(),
name: label.name,
albums_line,
image_url: crate::label::extract_label_image(label.image.as_ref()),
}
}
@@ -491,9 +500,16 @@ pub fn apply_favorites(window: &AppWindow, data: FavData) {
id: l.id.into(),
name: l.name.into(),
albums_line: l.albums_line.into(),
image_url: l.image_url.into(),
image: slint::Image::default(),
})
.collect();
state.set_labels(ModelRc::new(VecModel::from(rows)));
// `labels` is the full set the artwork pipeline targets;
// `labels-visible` (what the grid renders) shares it until a
// search filter forks it, so artwork stays live.
let model = ModelRc::new(VecModel::from(rows));
state.set_labels(model.clone());
state.set_labels_visible(model);
state.set_labels_total(total as i32);
}
}
@@ -967,7 +983,14 @@ pub fn artwork_jobs(data: &FavData) -> Vec<ArtworkJob> {
target: ArtworkTarget::FavoritePlaylist { index: i },
})
.collect(),
// Labels render an icon, no remote artwork.
FavData::Labels { .. } => Vec::new(),
FavData::Labels { items, .. } => items
.iter()
.enumerate()
.filter(|(_, l)| !l.image_url.is_empty())
.map(|(i, l)| ArtworkJob {
url: l.image_url.clone(),
target: ArtworkTarget::FavoriteLabel { index: i },
})
.collect(),
}
}
+3 -2
View File
@@ -112,8 +112,9 @@ where
/// Extract the best URL from /label/page's flexible image value. It
/// can be a bare string or an object with mega/extralarge/large/...
/// keys (mirrors the Svelte extraction order).
fn extract_label_image(image: Option<&serde_json::Value>) -> String {
/// keys (mirrors the Svelte extraction order). Reused by the favorites
/// Labels tab, whose wire `image` is a bare string per the Android DTO.
pub(crate) fn extract_label_image(image: Option<&serde_json::Value>) -> String {
let Some(image) = image else {
return String::new();
};
+5 -2
View File
@@ -1448,11 +1448,13 @@ export struct FavoritePlaylistItem {
cover: image,
}
// One label row in the Favorites > Labels list.
// One label card in the Favorites > Labels grid.
export struct FavoriteLabelItem {
id: string,
name: string,
albums-line: string, // "N releases"
albums-line: string, // "N albums"
image-url: string, // resolved URL ("" -> gradient placeholder)
image: image, // decoded cover, filled by the artwork pipeline
}
// Library > Favorites state. The active tab decides which model is
@@ -1486,6 +1488,7 @@ export global FavoritesState {
in property <[FavoriteArtistItem]> artists: [];
in property <[FavoritePlaylistItem]> playlists: [];
in property <[FavoriteLabelItem]> labels: [];
in property <[FavoriteLabelItem]> labels-visible: []; // after search filter (rendered)
in property <int> tracks-total: 0;
in property <int> albums-total: 0;
in property <int> artists-total: 0;