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
122 lines
4.3 KiB
Plaintext
122 lines
4.3 KiB
Plaintext
// 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";
|
|
import { QbzIcon } from "QbzIcon.slint";
|
|
|
|
export component QualityBadge inherits Rectangle {
|
|
// "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 : (root.tier == "lossless" ? 36px : 30px);
|
|
height: 30px;
|
|
|
|
if root.tier == "hires": Image {
|
|
source: @image-url("../assets/hi-res.svg");
|
|
width: 42px;
|
|
height: 28px;
|
|
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
|
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;
|
|
QbzIcon {
|
|
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
|
width: 16px;
|
|
height: 16px;
|
|
source: @image-url("../assets/cd.svg");
|
|
image-fit: contain;
|
|
// Tinted light, matching the Tauri CD badge.
|
|
tint: #b0b0b0;
|
|
}
|
|
}
|
|
if root.tier == "mp3": Rectangle {
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 3px;
|
|
background: Theme.surface-elevated;
|
|
border-width: 1px;
|
|
border-color: Theme.border-subtle;
|
|
QbzIcon {
|
|
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
|
width: 16px;
|
|
height: 16px;
|
|
source: @image-url("../assets/mp3.svg");
|
|
image-fit: contain;
|
|
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.
|
|
tip := Rectangle {
|
|
x: root.width - self.width;
|
|
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;
|
|
}
|
|
}
|