From 2a7147a00860504c9ad1842ec323d2408d29c013 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 18 May 2026 14:29:45 -0600 Subject: [PATCH] feat(qbz-slint): album card hover overlay Album cards gain the AlbumCardLite-style hover overlay: a dark scrim, genre/year meta in the top-left, and circular favorite / play / more action buttons (play primary, white). The action buttons are visual for now; one TouchArea over the artwork drives the hover state and emits a clicked(album_id) callback for navigation. AlbumCardItem carries genre and year; home.rs maps them from the discover album (genre name, year from the release dates). cc --- crates/qbz-slint/src/home.rs | 17 ++- crates/qbz-slint/ui/discover/AlbumCard.slint | 103 ++++++++++++++++++- crates/qbz-slint/ui/state.slint | 2 + 3 files changed, 117 insertions(+), 5 deletions(-) diff --git a/crates/qbz-slint/src/home.rs b/crates/qbz-slint/src/home.rs index a6e69a298..e236d82d7 100644 --- a/crates/qbz-slint/src/home.rs +++ b/crates/qbz-slint/src/home.rs @@ -24,6 +24,8 @@ pub struct CardData { pub id: String, pub title: String, pub artist: String, + pub genre: String, + pub year: String, /// "hires" | "cd" | "" — drives the icon-only quality badge. pub quality_tier: String, pub ribbon: String, @@ -83,7 +85,16 @@ fn map_album(album: DiscoverAlbum) -> CardData { .first() .map(|a| a.name.clone()) .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 (ribbon, ribbon_kind) = pick_ribbon(album.awards.as_deref()); + let quality_tier = quality_tier(album.audio_info.as_ref()).to_string(); let artwork_url = album .image .large @@ -94,7 +105,9 @@ fn map_album(album: DiscoverAlbum) -> CardData { id: album.id, title: album.title, artist, - quality_tier: quality_tier(album.audio_info.as_ref()).to_string(), + genre, + year, + quality_tier, ribbon, ribbon_kind, artwork_url, @@ -146,6 +159,8 @@ pub fn apply_sections(window: &AppWindow, data: Vec) { id: card.id.into(), title: card.title.into(), artist: card.artist.into(), + genre: card.genre.into(), + year: card.year.into(), quality_tier: card.quality_tier.into(), ribbon: card.ribbon.into(), ribbon_kind: card.ribbon_kind.into(), diff --git a/crates/qbz-slint/ui/discover/AlbumCard.slint b/crates/qbz-slint/ui/discover/AlbumCard.slint index 63ec7ee0c..6a4261b01 100644 --- a/crates/qbz-slint/ui/discover/AlbumCard.slint +++ b/crates/qbz-slint/ui/discover/AlbumCard.slint @@ -1,9 +1,10 @@ // Album card for the Discover sections. // // Mirrors the Svelte AlbumCardLite: 220px column, 220px square artwork -// with a bottom-left award ribbon, then a tight title / artist stack -// alongside an icon-only quality badge. Deterministic height (274px) so -// the section row never scrolls vertically. +// with a bottom-left award ribbon and a hover overlay (dark scrim, +// genre/year meta, and play / favorite / more action buttons), then a +// tight title / artist stack alongside an icon-only quality badge. +// Deterministic 266px height. import { Theme } from "../foundation/semantic-colors.slint"; import { Typography } from "../foundation/typography.slint"; @@ -11,8 +12,32 @@ import { Radius } from "../foundation/radius.slint"; import { Spacing } from "../foundation/spacing.slint"; import { AlbumCardItem } from "../state.slint"; +// Circular hover-overlay action button. +component OverlayButton inherits Rectangle { + in property glyph; + in property primary: false; + in property diameter: 36px; + in property glyph-size: 16px; + + width: root.diameter; + height: root.diameter; + border-radius: root.diameter / 2; + background: root.primary ? #ffffff : transparent; + border-width: root.primary ? 0px : 1.5px; + border-color: #ffffffe6; + + Text { + text: root.glyph; + color: root.primary ? #000000 : #ffffff; + font-size: root.glyph-size; + horizontal-alignment: center; + vertical-alignment: center; + } +} + export component AlbumCard inherits Rectangle { in property album; + callback clicked(string); // Fully deterministic height: artwork 220 + 6 spacer + 40 meta. width: 220px; @@ -22,7 +47,7 @@ export component AlbumCard inherits Rectangle { alignment: start; spacing: 0px; - // --- Artwork ------------------------------------------------------ + // --- Artwork + hover overlay ------------------------------------- Rectangle { width: 220px; height: 220px; @@ -39,6 +64,66 @@ export component AlbumCard inherits Rectangle { image-fit: cover; } + // Hover scrim. + Rectangle { + width: 100%; + height: 100%; + background: #000000; + opacity: hover.has-hover ? 0.6 : 0.0; + animate opacity { duration: 150ms; } + } + + // Hover meta — genre + year, top-left. + VerticalLayout { + x: 12px; + y: 12px; + spacing: 1px; + opacity: hover.has-hover ? 1.0 : 0.0; + animate opacity { duration: 150ms; } + + Text { + text: root.album.genre; + visible: root.album.genre != ""; + color: #ffffffeb; + font-size: 14px; + font-weight: Typography.medium; + } + Text { + text: root.album.year; + visible: root.album.year != ""; + color: #ffffffcc; + font-size: 13px; + } + } + + // Hover action buttons — favorite / play / more. + HorizontalLayout { + x: 0px; + y: 132px; + width: 220px; + height: 44px; + alignment: center; + spacing: 12px; + opacity: hover.has-hover ? 1.0 : 0.0; + animate opacity { duration: 150ms; } + + VerticalLayout { + alignment: center; + OverlayButton { glyph: "♡"; } + } + OverlayButton { + glyph: "▶"; + primary: true; + diameter: 44px; + glyph-size: 17px; + } + VerticalLayout { + alignment: center; + OverlayButton { glyph: "⋯"; glyph-size: 18px; } + } + } + + // Award ribbon — always visible, above the overlay. if root.album.ribbon != "": Rectangle { x: 0px; y: parent.height - self.height - 8px; @@ -72,6 +157,16 @@ export component AlbumCard inherits Rectangle { } } } + + // Hover + click detector — topmost so it owns the pointer. + hover := TouchArea { + width: 100%; + height: 100%; + mouse-cursor: pointer; + clicked => { + root.clicked(root.album.id); + } + } } Rectangle { height: 6px; } diff --git a/crates/qbz-slint/ui/state.slint b/crates/qbz-slint/ui/state.slint index 765cfaeab..6afaf72aa 100644 --- a/crates/qbz-slint/ui/state.slint +++ b/crates/qbz-slint/ui/state.slint @@ -7,6 +7,8 @@ export struct AlbumCardItem { id: string, title: string, artist: string, + genre: string, // shown in the hover overlay + year: string, // shown in the hover overlay quality-tier: string, // "hires" | "cd" | "" — drives the badge icon ribbon: string, ribbon-kind: string, // "press" | "qobuzissime" | "albumOfTheWeek" | ""