feat(slint): album cards show genre + localized full release date (#469)

Album-card hover overlay now shows the full release date as "MMM D, YYYY"
(e.g. "Nov 6, 2025") instead of the year alone. New dates::release_label
helper keeps the month/day/year order fixed but localizes the month via
chrono format_localized (the app ships 5 languages; current_locale() is the
single hook to thread the real UI language once Slint wires translations —
English for now). Applied to the Discover and Favorites album mappings.

Also fixes Favorites > Albums cards, which hardcoded an empty genre and a
year-only label: they now read album.genre and the full date, matching the
Discover cards 1:1. The data was already in the getUserFavorites response
(qbz-models Album parses genre + release_date_original) — no backend change.

cc
This commit is contained in:
vicrodh committed 2026-05-25 17:42:36 -06:00
1 parent 21af0f63f8
commit 9f1e036308
6 files changed
+65 -13

No files matched your search

+1
View File
@@ -35,6 +35,7 @@ rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "t
serde = { workspace = true }
serde_json = { workspace = true }
dirs = "5"
chrono = { workspace = true, features = ["unstable-locales"] }
qbz-app = { path = "../qbz-app" }
qbz-audio = { path = "../qbz-audio" }
qbz-core = { path = "../qbz-core" }
+43
View File
@@ -0,0 +1,43 @@
//! Release-date label formatting for album cards (#469).
//!
//! Renders a Qobuz release date as a fixed "MMM D, YYYY" structure (e.g.
//! "Nov 6, 2025") with a *localized* abbreviated month. We deliberately keep
//! the month / day / year order fixed (not the locale-reordered output of a
//! `toLocaleDateString`-style call) but localize the month token, because the
//! app ships five UI languages (en / es / de / fr / pt). The Slint UI is
//! English-only during the migration, so [`current_locale`] returns English
//! today — it is the single place to wire the real UI language once Slint
//! gets translation support, and no caller needs to change when it does.
use chrono::{Locale, NaiveDate};
/// UI language used to render date labels. Slint is English-only for now;
/// this is the one hook to thread the persisted UI language through later
/// (the app supports en / es / de / fr / pt).
pub fn current_locale() -> Locale {
Locale::en_US
}
/// Format a Qobuz release date string ("YYYY-MM-DD", possibly with a trailing
/// time component) as "MMM D, YYYY" with a localized month. Falls back to the
/// bare 4-digit year when only a year is available or the value cannot be
/// parsed as a full date, and to an empty string when there is no date.
pub fn release_label(date: Option<&str>) -> String {
let Some(raw) = date else {
return String::new();
};
let raw = raw.trim();
if raw.is_empty() {
return String::new();
}
// Only the leading YYYY-MM-DD matters; ignore any trailing time.
let head = raw.get(0..10).unwrap_or(raw);
if let Ok(parsed) = NaiveDate::parse_from_str(head, "%Y-%m-%d") {
// %b = localized abbreviated month, %-d = day without leading zero.
return parsed
.format_localized("%b %-d, %Y", current_locale())
.to_string();
}
// Year-only fallback (e.g. the source only had "2025").
raw.get(0..4).unwrap_or_default().to_string()
}
+5 -6
View File
@@ -104,6 +104,7 @@ pub struct AlbumCard {
pub title: String,
pub artist: String,
pub artist_id: String,
pub genre: String,
pub year: String,
pub quality_tier: String,
pub quality_label: String,
@@ -240,11 +241,8 @@ fn map_track(track: Track) -> TrackCard {
}
fn map_album(album: Album) -> AlbumCard {
let year = album
.release_date_original
.as_deref()
.and_then(|s| s.get(..4).map(|y| y.to_string()))
.unwrap_or_default();
let year = crate::dates::release_label(album.release_date_original.as_deref());
let genre = album.genre.map(|g| g.name).unwrap_or_default();
let quality_label = match (album.maximum_bit_depth, album.maximum_sampling_rate) {
(Some(bd), Some(sr)) => format!("{}-bit / {} kHz", bd, sr),
_ => String::new(),
@@ -254,6 +252,7 @@ fn map_album(album: Album) -> AlbumCard {
title: album.title,
artist: album.artist.name,
artist_id: album.artist.id.to_string(),
genre,
year,
quality_tier: tier(album.maximum_bit_depth).to_string(),
quality_label,
@@ -338,7 +337,7 @@ pub fn apply_favorites(window: &AppWindow, data: FavData) {
title: a.title.into(),
artist: a.artist.into(),
artist_id: a.artist_id.into(),
genre: "".into(),
genre: a.genre.into(),
year: a.year.into(),
quality_tier: a.quality_tier.into(),
quality_label: a.quality_label.into(),
+7 -7
View File
@@ -230,13 +230,13 @@ fn map_album(album: DiscoverAlbum) -> CardData {
.map(|a| a.id.to_string())
.unwrap_or_default();
let genre = album.genre.map(|g| g.name).unwrap_or_default();
let year = album
.dates
.as_ref()
.and_then(|d| d.original.as_ref().or(d.download.as_ref()).or(d.stream.as_ref()))
.and_then(|date| date.get(0..4))
.unwrap_or("")
.to_string();
let year = crate::dates::release_label(
album
.dates
.as_ref()
.and_then(|d| d.original.as_ref().or(d.download.as_ref()).or(d.stream.as_ref()))
.map(|s| s.as_str()),
);
let (ribbon, ribbon_kind) = pick_ribbon(album.awards.as_deref());
let quality_tier = quality_tier(album.audio_info.as_ref()).to_string();
let quality_label = quality_label(album.audio_info.as_ref());
+1
View File
@@ -21,6 +21,7 @@ mod artwork;
mod auth;
mod commands;
mod custom_artwork;
mod dates;
mod discovery_dismiss;
mod favorites;
mod foryou;