Accumulated qt-frontend work from the 2026-08-11/12 sessions (cc), uncommitted until now: the app-wide dynamic background in both modes (AmbientField with GPU/Canvas arms, AppShell layering, ambient palette pipeline), the wave-1 clip/batch sweep across the tree (alpha batches 449 -> 327, reproducible), the rounded-cover and icon-tint arms, and scripts/qt-batches.sh (batches + presents/s sampler). The GPU behaviour of these pieces was finalized in the follow-up pulse commits; see qbz-nix-docs/qt-frontend/2026-08-11-scenegraph-batches/ GPU-COST-INVESTIGATION.md for the full campaign. km
151 lines
6.3 KiB
QML
151 lines
6.3 KiB
QML
// QualityBadgeFull — 1:1 port of
|
|
// crates/qbz-ui/ui/primitives/QualityBadgeFull.slint.
|
|
//
|
|
// The contained pill: format icon + the tier label stacked over the exact
|
|
// bit-depth / sample-rate line. This is what the New and Classic now-playing
|
|
// bars mount (inside the song card's audio stamp) and what album/track rows
|
|
// use to advertise stored quality. The Small and Large bars mount
|
|
// shell/AudioStamp.qml instead — a different component entirely.
|
|
//
|
|
// Numbers, read off the .slint (sf = scaleFactor):
|
|
// height (compact 29 | 36) * sf
|
|
// min width (compact 108 | 136) * sf, or content width when `bare`
|
|
// radius 4, surface-hover fill, 1px border-subtle (all dropped when `bare`)
|
|
// padding (compact 8 | 10) * sf spacing (compact 5 | 6) * sf
|
|
// icon hires (compact 19 | 24) * sf cd/mp3/lossless (compact 13 | 16) * sf
|
|
// tier text (compact 7 | 8) * sf, weight 100, letter-spacing 0.5, secondary
|
|
// detail (compact 8 | 9) * sf, weight 100, muted
|
|
|
|
import QtQuick
|
|
import com.blitzfc.qbz
|
|
import "../theme"
|
|
|
|
Item {
|
|
id: root
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
/// "hires" | "cd" | "mp3" | "lossless" | "".
|
|
property string tier: ""
|
|
/// Exact quality, e.g. "24-bit / 96 kHz".
|
|
property string detail: ""
|
|
/// Show the format mark. The track-row variant turns it off.
|
|
property bool showIcon: true
|
|
/// No chip background or border, text centred — blends into a track row.
|
|
property bool bare: false
|
|
/// ~20%-smaller variant for the now-playing stamp.
|
|
property bool compact: false
|
|
/// Extra proportional scale on top of `compact` (1.0 = none).
|
|
property real scaleFactor: 1.0
|
|
/// Cap for the text column, in px. 0 = uncapped, which is what every
|
|
/// existing call site gets — this exists so the ONE caller that used to
|
|
/// wrap the badge in a `clip: true` (rows/TrackRow.qml's quality cell) can
|
|
/// bound the badge instead. A clip there was a batch root per visible row,
|
|
/// and Qt cannot merge alpha geometry across clip roots.
|
|
property real maxTextWidth: 0
|
|
|
|
readonly property string tierLabel: tier === "hires" ? "HI-RES"
|
|
: (tier === "mp3" ? "MP3"
|
|
: (tier === "lossless" ? "LOSSLESS" : "CD"))
|
|
|
|
readonly property bool isHiRes: tier === "hires"
|
|
readonly property real sf: scaleFactor
|
|
readonly property real padH: bare ? 0 : (compact ? 8 : 10) * sf
|
|
readonly property real gap: bare ? 0 : (compact ? 5 : 6) * sf
|
|
readonly property real iconSide: isHiRes ? (compact ? 19 : 24) * sf
|
|
: (compact ? 13 : 16) * sf
|
|
// The row's own width (no paddings) — the icon slot plus the text column.
|
|
readonly property real innerWidth: (showIcon ? iconSide + gap : 0) + textCol.width
|
|
|
|
visible: tier !== ""
|
|
implicitHeight: (compact ? 29 : 36) * sf
|
|
implicitWidth: bare ? innerWidth
|
|
: Math.max(innerWidth + 2 * padH, (compact ? 108 : 136) * sf)
|
|
width: implicitWidth
|
|
height: implicitHeight
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: 4
|
|
color: root.bare ? "transparent" : theme.surfaceHover
|
|
border.width: root.bare ? 0 : 1
|
|
border.color: theme.borderSubtle
|
|
}
|
|
|
|
Row {
|
|
id: content
|
|
// `alignment: start` past the left padding; `alignment: center` when
|
|
// bare (no padding to start from).
|
|
x: root.bare ? Math.round((root.width - root.innerWidth) / 2) : root.padH
|
|
height: parent.height
|
|
spacing: root.showIcon ? root.gap : 0
|
|
|
|
// Format mark. The Hi-Res brand mark keeps its own colours; the CD /
|
|
// MP3 / LOSSLESS marks render in the themed muted tier.
|
|
Item {
|
|
visible: root.showIcon
|
|
width: root.showIcon ? root.iconSide : 0
|
|
height: parent.height
|
|
|
|
Image {
|
|
visible: root.isHiRes
|
|
anchors.centerIn: parent
|
|
source: "qrc:/qt/qml/com/blitzfc/qbz/qml/assets/hi-res.svg"
|
|
width: root.iconSide
|
|
height: root.iconSide
|
|
fillMode: Image.PreserveAspectFit
|
|
sourceSize: Qt.size(Math.round(root.iconSide * 2), Math.round(root.iconSide * 2))
|
|
}
|
|
// Un-hydrated lossless reuses the neutral disc mark; the detail
|
|
// line carries "FLAC".
|
|
QbzIcon {
|
|
visible: !root.isHiRes
|
|
anchors.centerIn: parent
|
|
name: root.tier === "mp3" ? "mp3" : "cd"
|
|
tintName: "muted"
|
|
width: root.iconSide
|
|
height: root.iconSide
|
|
}
|
|
}
|
|
|
|
Column {
|
|
id: textCol
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 1
|
|
// Explicit (not implicit) so the children may bind to it: a Text
|
|
// with `width: parent.width` inside an implicitly-sized Column is
|
|
// a binding loop. Both Texts are unwrapped, so `implicitWidth` is
|
|
// the natural content width and does not depend on `width`.
|
|
// `implicitWidth` stays the natural content width even with the
|
|
// elides below (an unwrapped Text does not shrink its implicit
|
|
// size), so the no-binding-loop argument above still holds.
|
|
width: root.maxTextWidth > 0
|
|
? Math.min(root.maxTextWidth,
|
|
Math.max(tierText.implicitWidth, detailText.implicitWidth))
|
|
: Math.max(tierText.implicitWidth, detailText.implicitWidth)
|
|
|
|
Text {
|
|
id: tierText
|
|
width: parent.width
|
|
text: root.tierLabel
|
|
color: theme.textSecondary
|
|
font.pixelSize: Math.max(1, Math.round((root.compact ? 7 : 8) * root.sf))
|
|
font.weight: Font.Thin
|
|
font.letterSpacing: 0.5
|
|
horizontalAlignment: root.bare ? Text.AlignHCenter : Text.AlignLeft
|
|
elide: Text.ElideRight
|
|
}
|
|
Text {
|
|
id: detailText
|
|
width: parent.width
|
|
text: root.detail
|
|
color: theme.textMuted
|
|
font.pixelSize: Math.max(1, Math.round((root.compact ? 8 : 9) * root.sf))
|
|
font.weight: Font.Thin
|
|
horizontalAlignment: root.bare ? Text.AlignHCenter : Text.AlignLeft
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
}
|
|
}
|