fix(slint): album card overlay, award ribbon, and quality tooltip
Three corrections to the Discover album card:
- Overlay meta: the genre is one point larger and bold, and the year
now sits a single line below it (conditional `if` rows with explicit
heights, instead of `visible` toggles that left a phantom gap)
- Award ribbon: width hugs its label (capped at 200px, below the card
width) instead of spanning the full card — it reads as a ribbon again
- Quality badge: extracted to a QualityBadge primitive that reveals
the exact quality ("Hi-Res: 24-bit / 96 kHz") on hover, mirroring the
Tauri QualityBadge; home.rs derives the label from the discover
audio info
cc
This commit is contained in:
6 files changed
+138
-33
No files matched your search
Generated
+1
@@ -5454,6 +5454,7 @@ dependencies = [
|
||||
"log",
|
||||
"open",
|
||||
"qbz-app",
|
||||
"qbz-audio",
|
||||
"qbz-cache",
|
||||
"qbz-core",
|
||||
"qbz-credentials",
|
||||
|
||||
@@ -120,6 +120,7 @@ fn map_release(release: PageArtistRelease) -> CardData {
|
||||
genre: release.genre.map(|g| g.name).unwrap_or_default(),
|
||||
year: String::new(),
|
||||
quality_tier: String::new(),
|
||||
quality_label: String::new(),
|
||||
ribbon: String::new(),
|
||||
ribbon_kind: String::new(),
|
||||
artwork_url,
|
||||
@@ -182,6 +183,7 @@ pub fn apply_artist(window: &AppWindow, data: ArtistData) {
|
||||
genre: card.genre.into(),
|
||||
year: card.year.into(),
|
||||
quality_tier: card.quality_tier.into(),
|
||||
quality_label: slint::SharedString::new(),
|
||||
ribbon: card.ribbon.into(),
|
||||
ribbon_kind: card.ribbon_kind.into(),
|
||||
artwork_url: card.artwork_url.into(),
|
||||
|
||||
@@ -33,6 +33,8 @@ pub struct CardData {
|
||||
pub year: String,
|
||||
/// "hires" | "cd" | "" — drives the icon-only quality badge.
|
||||
pub quality_tier: String,
|
||||
/// "Hi-Res: 24-bit / 96 kHz" — shown when hovering the quality badge.
|
||||
pub quality_label: String,
|
||||
pub ribbon: String,
|
||||
pub ribbon_kind: String,
|
||||
pub artwork_url: String,
|
||||
@@ -119,6 +121,7 @@ fn map_album(album: DiscoverAlbum) -> CardData {
|
||||
.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 quality_label = quality_label(album.audio_info.as_ref());
|
||||
let artwork_url = album
|
||||
.image
|
||||
.large
|
||||
@@ -132,6 +135,7 @@ fn map_album(album: DiscoverAlbum) -> CardData {
|
||||
genre,
|
||||
year,
|
||||
quality_tier,
|
||||
quality_label,
|
||||
ribbon,
|
||||
ribbon_kind,
|
||||
artwork_url,
|
||||
@@ -191,6 +195,34 @@ fn quality_tier(audio: Option<&DiscoverAudioInfo>) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// Exact-quality label for the badge hover tooltip, mirroring the Tauri
|
||||
/// `QualityBadge` (`{tier}: {depth}-bit / {rate} kHz`). Empty when the
|
||||
/// discover entry carries no audio info, matching `quality_tier`.
|
||||
fn quality_label(audio: Option<&DiscoverAudioInfo>) -> String {
|
||||
let Some(audio) = audio else {
|
||||
return String::new();
|
||||
};
|
||||
let hi_res = matches!(audio.maximum_bit_depth, Some(depth) if depth >= 24);
|
||||
let tier = if hi_res { "Hi-Res" } else { "CD" };
|
||||
let depth = audio
|
||||
.maximum_bit_depth
|
||||
.unwrap_or(if hi_res { 24 } else { 16 });
|
||||
let rate = audio
|
||||
.maximum_sampling_rate
|
||||
.unwrap_or(if hi_res { 96.0 } else { 44.1 });
|
||||
format!("{tier}: {depth}-bit / {} kHz", format_rate(rate))
|
||||
}
|
||||
|
||||
/// Format a kHz sample rate without a trailing `.0` (96.0 -> "96",
|
||||
/// 44.1 -> "44.1").
|
||||
fn format_rate(rate: f64) -> String {
|
||||
if (rate.fract()).abs() < f64::EPSILON {
|
||||
format!("{}", rate as i64)
|
||||
} else {
|
||||
format!("{rate}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert worker-thread home data into Slint models and push them onto
|
||||
/// the `HomeState` global. Must run on the Slint event loop.
|
||||
pub fn apply_home(window: &AppWindow, data: HomeData) {
|
||||
@@ -208,6 +240,7 @@ pub fn apply_home(window: &AppWindow, data: HomeData) {
|
||||
genre: card.genre.into(),
|
||||
year: card.year.into(),
|
||||
quality_tier: card.quality_tier.into(),
|
||||
quality_label: card.quality_label.into(),
|
||||
ribbon: card.ribbon.into(),
|
||||
ribbon_kind: card.ribbon_kind.into(),
|
||||
artwork_url: card.artwork_url.into(),
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Radius } from "../foundation/radius.slint";
|
||||
import { Spacing } from "../foundation/spacing.slint";
|
||||
import { AlbumCardItem } from "../state.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { QualityBadge } from "../primitives/QualityBadge.slint";
|
||||
|
||||
// Circular hover-overlay action button.
|
||||
component OverlayButton inherits Rectangle {
|
||||
@@ -75,26 +76,29 @@ export component AlbumCard inherits Rectangle {
|
||||
animate opacity { duration: 150ms; }
|
||||
}
|
||||
|
||||
// Hover meta — genre + year, top-left.
|
||||
// Hover meta — genre + year, top-left. The year sits one
|
||||
// single line below the genre.
|
||||
VerticalLayout {
|
||||
x: 12px;
|
||||
y: 12px;
|
||||
spacing: 1px;
|
||||
spacing: 2px;
|
||||
opacity: hover.has-hover ? 1.0 : 0.0;
|
||||
animate opacity { duration: 150ms; }
|
||||
|
||||
Text {
|
||||
if root.album.genre != "": Text {
|
||||
text: root.album.genre;
|
||||
visible: root.album.genre != "";
|
||||
height: 20px;
|
||||
color: #ffffffeb;
|
||||
font-size: 14px;
|
||||
font-weight: Typography.medium;
|
||||
font-size: 15px;
|
||||
font-weight: Typography.bold;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
Text {
|
||||
if root.album.year != "": Text {
|
||||
text: root.album.year;
|
||||
visible: root.album.year != "";
|
||||
height: 17px;
|
||||
color: #ffffffcc;
|
||||
font-size: 13px;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,16 +129,21 @@ export component AlbumCard inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Award ribbon — always visible, above the overlay.
|
||||
// Award ribbon — content-width only (it is a ribbon, not a
|
||||
// full-width band), capped so long labels still elide.
|
||||
if root.album.ribbon != "": Rectangle {
|
||||
x: 0px;
|
||||
y: parent.height - self.height - 8px;
|
||||
height: 20px;
|
||||
// Hug the label; cap below the card width so a runaway
|
||||
// label elides instead of spanning the whole card.
|
||||
width: Math.min(ribbon-row.preferred-width, 200px);
|
||||
background: root.album.ribbon-kind == "press"
|
||||
? #d49511
|
||||
: #000000e0;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
clip: true;
|
||||
|
||||
// Left accent stripe (not on the gold press ribbon).
|
||||
Rectangle {
|
||||
@@ -144,7 +153,7 @@ export component AlbumCard inherits Rectangle {
|
||||
? #8b5cf6
|
||||
: #eab308;
|
||||
}
|
||||
HorizontalLayout {
|
||||
ribbon-row := HorizontalLayout {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
Text {
|
||||
@@ -202,31 +211,12 @@ export component AlbumCard inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Icon-only quality badge.
|
||||
// Icon-only quality badge with an exact-quality hover tooltip.
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
|
||||
if root.album.quality-tier == "hires": Image {
|
||||
source: @image-url("../assets/hi-res.svg");
|
||||
width: 42px;
|
||||
height: 28px;
|
||||
image-fit: contain;
|
||||
}
|
||||
if root.album.quality-tier == "cd": Rectangle {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 3px;
|
||||
background: Theme.surface-elevated;
|
||||
border-width: 1px;
|
||||
border-color: Theme.border-subtle;
|
||||
Image {
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
source: @image-url("../assets/cd.svg");
|
||||
image-fit: contain;
|
||||
}
|
||||
QualityBadge {
|
||||
tier: root.album.quality-tier;
|
||||
label: root.album.quality-label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// Icon-only quality badge with a hover tooltip.
|
||||
//
|
||||
// The square badge is intentionally compact (it buys layout space); the
|
||||
// exact quality — "Hi-Res: 24-bit / 96 kHz" — is revealed on hover, the
|
||||
// same string the Tauri QualityBadge surfaces.
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
|
||||
export component QualityBadge inherits Rectangle {
|
||||
// "hires" | "cd" | "" — empty hides the badge entirely.
|
||||
in property <string> tier;
|
||||
// Exact quality string for the hover tooltip.
|
||||
in property <string> label;
|
||||
|
||||
visible: root.tier != "";
|
||||
width: root.tier == "hires" ? 42px : 30px;
|
||||
height: 30px;
|
||||
|
||||
if root.tier == "hires": Image {
|
||||
source: @image-url("../assets/hi-res.svg");
|
||||
width: 42px;
|
||||
height: 28px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
image-fit: contain;
|
||||
}
|
||||
if root.tier == "cd": Rectangle {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 3px;
|
||||
background: Theme.surface-elevated;
|
||||
border-width: 1px;
|
||||
border-color: Theme.border-subtle;
|
||||
Image {
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
source: @image-url("../assets/cd.svg");
|
||||
image-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
// Hover tooltip — floats just above the badge.
|
||||
tip := Rectangle {
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: -self.height - 6px;
|
||||
width: tip-row.preferred-width;
|
||||
height: 24px;
|
||||
background: Theme.surface-elevated;
|
||||
border-radius: Radius.sm;
|
||||
border-width: 1px;
|
||||
border-color: Theme.border-subtle;
|
||||
drop-shadow-blur: 14px;
|
||||
drop-shadow-color: Theme.card-shadow;
|
||||
drop-shadow-offset-y: 3px;
|
||||
opacity: ta.has-hover && root.label != "" ? 1.0 : 0.0;
|
||||
animate opacity { duration: 120ms; }
|
||||
|
||||
tip-row := HorizontalLayout {
|
||||
padding-left: 9px;
|
||||
padding-right: 9px;
|
||||
Text {
|
||||
text: root.label;
|
||||
color: Theme.text-secondary;
|
||||
font-size: 11px;
|
||||
font-weight: Typography.medium;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ta := TouchArea {
|
||||
mouse-cursor: default;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export struct AlbumCardItem {
|
||||
genre: string, // shown in the hover overlay
|
||||
year: string, // shown in the hover overlay
|
||||
quality-tier: string, // "hires" | "cd" | "" — drives the badge icon
|
||||
quality-label: string, // "Hi-Res: 24-bit / 96 kHz" — badge hover tooltip
|
||||
ribbon: string,
|
||||
ribbon-kind: string, // "press" | "qobuzissime" | "albumOfTheWeek" | ""
|
||||
artwork-url: string,
|
||||
|
||||
Reference in new issue
Block a user