feat(slint): Plex quality hydration on album-open + consistent quality badges (port slice 6)

Hydration:
- qbz-plex: additive plex_hydrate_album_quality(base, token, rating_keys) — per
  key plex_get_track_metadata (batch 5, 5s/call timeout, skip failures) then the
  COALESCE write-back plex_cache_update_track_quality; awaited before re-read.
- qbz-player/qbz-core: patch_plex_quality / patch_plex_queue_quality patch the
  in-memory queue in place by rating_key (for the now-playing stamp).
- qbz-slint: album-open trigger (gated on DB-NULL, not a value heuristic) then
  refresh of all badge surfaces — album-card grid (re-read aggregate + patch
  every model + the raw filter source), album-detail header + rows, now-playing
  stamp. Re-sync stays routed through plex_cache_save_tracks (carry-over).

Quality badge consistency (the four surfaces must agree — and pre-hydration
show the filetype, not nothing):
- One shared classifier crate::quality::badge(format, bit_depth, sample_rate) ->
  (tier, detail, tooltip); the album card, album-detail header and track rows
  all route through it (kills the per-surface duplication that produced
  "96 kHz" vs "96000 kHz"). sample_rate normalized to kHz via a >=1000 guard so
  Qobuz's already-kHz values are never re-divided (no "0.041 kHz").
- New "lossless" tier: an un-hydrated lossless file (FLAC/WAV/ALAC… with unknown
  bit depth) shows a generic filetype badge ("FLAC") instead of an empty badge,
  rendered in QualityBadge (compact chip) + QualityBadgeFull; upgrades to
  "24-bit / 96 kHz" after hydration. Local/Qobuz/mp3 unchanged.

cc
This commit is contained in:
vicrodh committed 2026-06-08 10:27:28 -06:00
1 parent a680da5ad8
commit 5939c090ca
10 files changed
+599 -38

No files matched your search

@@ -373,6 +373,7 @@ export component AlbumCard inherits Rectangle {
QualityBadge {
tier: root.album.quality-tier;
label: root.album.quality-label;
detail: root.album.quality-detail;
}
}
}
@@ -10,13 +10,16 @@ import { Radius } from "../foundation/radius.slint";
import { QbzIcon } from "QbzIcon.slint";
export component QualityBadge inherits Rectangle {
// "hires" | "cd" | "mp3" | "" — empty hides the badge entirely.
// "hires" | "cd" | "mp3" | "lossless" | "" — empty hides the badge entirely.
in property <string> tier;
// Exact quality string for the hover tooltip.
in property <string> label;
// For the "lossless" tier (un-hydrated filetype, e.g. an un-hydrated Plex
// FLAC): the bare filetype to print, e.g. "FLAC".
in property <string> detail;
visible: root.tier != "";
width: root.tier == "hires" ? 42px : 30px;
width: root.tier == "hires" ? 42px : (root.tier == "lossless" ? 36px : 30px);
height: 30px;
if root.tier == "hires": Image {
@@ -62,6 +65,25 @@ export component QualityBadge inherits Rectangle {
tint: #b0b0b0;
}
}
// Un-hydrated lossless: the bit-depth/rate isn't known yet, but the
// filetype is — show it as a neutral text chip (e.g. "FLAC") rather than
// disappearing. Upgrades to the cd/hires badge once hydrated.
if root.tier == "lossless": Rectangle {
width: 36px;
height: 30px;
border-radius: 3px;
background: Theme.surface-elevated;
border-width: 1px;
border-color: Theme.border-subtle;
Text {
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
text: root.detail;
color: #b0b0b0;
font-size: 9px;
font-weight: Typography.medium;
}
}
// Hover tooltip — floats just above the badge, right-anchored so it
// extends leftward into the card and is never clipped by the next card.
@@ -30,7 +30,9 @@ export component QualityBadgeFull inherits Rectangle {
property <string> tier-label: root.tier == "hires"
? "HI-RES"
: (root.tier == "mp3" ? "MP3" : "CD");
: (root.tier == "mp3"
? "MP3"
: (root.tier == "lossless" ? "LOSSLESS" : "CD"));
visible: root.tier != "";
height: (root.compact ? 29px : 36px) * root.scale-factor;
@@ -70,6 +72,14 @@ export component QualityBadgeFull inherits Rectangle {
height: (root.compact ? 13px : 16px) * root.scale-factor;
tint: #b0b0b0;
}
// Un-hydrated lossless (filetype known, bit-depth/rate not yet):
// reuse the neutral disc mark; the detail line carries "FLAC".
if root.tier == "lossless": QbzIcon {
source: @image-url("../assets/cd.svg");
width: (root.compact ? 13px : 16px) * root.scale-factor;
height: (root.compact ? 13px : 16px) * root.scale-factor;
tint: #b0b0b0;
}
}
VerticalLayout {