feat(qbz-qt): the quality stamp, ported from Slint per bar mode
The port had one hand-made badge taken from the archived Tauri tree. Slint has three distinct components and each bar mounts a specific one, so the wrong tree had cost the stamp its LEDs entirely — they exist only in Slint, derived from the output backend and mode, and no amount of polishing the Svelte port would have produced them. QualityBadge becomes Slint's icon-only mark with its hover tooltip, for cards and rows. QualityBadgeFull is the icon plus stacked tier and detail. AudioStamp is the inline two-row form that carries the LEDs: backend blue when engaged, mode green, purple for a cast session, amber with the renderer name for a peer Qobuz Connect. New and Classic mount the SongCard stamp column, Large and Small the inline stamp, matching the .slint mount sites. The downgrade case follows Slint too: row one reports what is actually being delivered with an amber arrow, and the catalog maximum moves into the tooltip next to the reason it was limited. PlayerBar's LEDs stop reading the settings document, which never knew whether a backend was engaged, and read the now-playing output state instead — where Slint reads them. cc
This commit is contained in:
9 files changed
+876
-445
No files matched your search
@@ -0,0 +1,272 @@
|
||||
// SongCardStamp — the IN-CARD audio stamp of crates/qbz-ui/ui/shell/
|
||||
// SongCard.slint (its `if root.has-stamp` column plus the DotLed it declares).
|
||||
//
|
||||
// This is what the NEW (0) and CLASSIC (1) now-playing bars mount, pinned to
|
||||
// the song card's right edge:
|
||||
//
|
||||
// [ QualityBadgeFull: icon + TIER over "24-bit / 96 kHz" ] 38px tall
|
||||
// (4px)
|
||||
// [ (o) PIPEWIRE (o) BITPERF ] 15px tall
|
||||
//
|
||||
// The SMALL (2) and LARGE (3) bars mount shell/AudioStamp.qml instead — the
|
||||
// inline 2-row form with the 7px text LEDs. They are different components in
|
||||
// Slint and stay different here; do not merge them.
|
||||
//
|
||||
// Geometry read off SongCard.slint with `compact-stamp: false` and
|
||||
// `narrow: false` (what both mounts pass): stamp-w 128px (dot-leds arm),
|
||||
// badge box 38px, LED row 15px, 4px between them.
|
||||
//
|
||||
// While the stream is downgraded the badge advertises the DELIVERED
|
||||
// tier/detail with an amber overlaid arrow, and the catalog max moves into the
|
||||
// tooltip's "Source" line next to the cause.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import com.blitzfc.qbz
|
||||
import "../controls"
|
||||
import "../theme"
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
spacing: 4
|
||||
width: 128
|
||||
|
||||
// SongCard.slint's show-delivered / quality-cause-line /
|
||||
// quality-stamp-tooltip, verbatim.
|
||||
readonly property bool showDelivered: QbzPlayer.npQualityDowngraded
|
||||
&& QbzPlayer.npQualityTrueDetail !== ""
|
||||
readonly property string qualityCauseLine: {
|
||||
var c = QbzPlayer.npQualityLimitCause
|
||||
if (c === 1) return QbzSession.tr("Limited by your streaming quality setting", QbzSession.trRev)
|
||||
if (c === 2) return QbzSession.tr("Limited by your output device's limit", QbzSession.trRev)
|
||||
if (c === 3) return QbzSession.tr("Limited by the quality cap for {}", QbzSession.trRev)
|
||||
.replace("{}", QbzPlayer.npCastTarget)
|
||||
if (c === 4) return QbzSession.tr("Qobuz has no higher quality for this track", QbzSession.trRev)
|
||||
return ""
|
||||
}
|
||||
readonly property string qualityStampTooltip: {
|
||||
if (!showDelivered) return QbzPlayer.npQualityDetail
|
||||
var t = QbzSession.tr("Source: {}", QbzSession.trRev).replace("{}", QbzPlayer.npQualityDetail)
|
||||
+ "\n"
|
||||
+ QbzSession.tr("Output: {}", QbzSession.trRev).replace("{}", QbzPlayer.npQualityTrueDetail)
|
||||
return qualityCauseLine === "" ? t : t + "\n" + qualityCauseLine
|
||||
}
|
||||
|
||||
// Dot LED (circle-dot on / circle off) + constant-colour label —
|
||||
// SongCard.slint's DotLed. 9px mark, 4px gap, 8px/600 label at
|
||||
// letter-spacing 0.3 that NEVER changes colour (it stays text-muted; only
|
||||
// the dot signals state).
|
||||
//
|
||||
// The mark is DRAWN, not an SVG: the port has no circle / circle-dot
|
||||
// assets, and the software path cannot recolour at render time, so every
|
||||
// LED colour would need its own baked variant. A 1px ring plus a 4px
|
||||
// centre dot is the same figure.
|
||||
component DotLed: Item {
|
||||
id: led
|
||||
property string label: ""
|
||||
property bool on: false
|
||||
property color onColor: "#22c55e"
|
||||
property string tooltip: ""
|
||||
implicitWidth: ledRow.implicitWidth
|
||||
implicitHeight: ledRow.implicitHeight
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
Row {
|
||||
id: ledRow
|
||||
spacing: 4
|
||||
Rectangle {
|
||||
width: 9
|
||||
height: 9
|
||||
radius: 4.5
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: "transparent"
|
||||
border.width: 1
|
||||
border.color: led.on ? led.onColor : theme.textMuted
|
||||
Rectangle {
|
||||
visible: led.on
|
||||
width: 4
|
||||
height: 4
|
||||
radius: 2
|
||||
anchors.centerIn: parent
|
||||
color: led.onColor
|
||||
}
|
||||
}
|
||||
Text {
|
||||
text: led.label
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 8
|
||||
font.weight: theme.weightSemibold
|
||||
font.letterSpacing: 0.3
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: ledHover
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
ToolTip {
|
||||
id: ledTip
|
||||
visible: ledHover.containsMouse && led.tooltip !== ""
|
||||
text: led.tooltip
|
||||
delay: 0
|
||||
timeout: -1
|
||||
padding: 0
|
||||
x: Math.round((led.width - width) / 2)
|
||||
y: -height - 6
|
||||
contentItem: Text {
|
||||
text: ledTip.text
|
||||
color: theme.textSecondary
|
||||
font.pixelSize: 11
|
||||
font.weight: theme.weightMedium
|
||||
leftPadding: 9
|
||||
rightPadding: 9
|
||||
topPadding: 5
|
||||
bottomPadding: 5
|
||||
}
|
||||
background: Rectangle {
|
||||
color: theme.surfaceElevated
|
||||
radius: theme.radiusSm
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- the quality badge + the downgrade arrow overlay --------------------
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 38
|
||||
|
||||
QualityBadgeFull {
|
||||
// Explicit width/height (SongCard.slint sets x/y/width/height on
|
||||
// the badge), NOT anchors.fill: the component binds its own
|
||||
// width/height to its implicit size, and a call-site assignment
|
||||
// cleanly replaces that binding where anchors would fight it.
|
||||
x: 0
|
||||
y: 0
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
compact: false
|
||||
showIcon: true
|
||||
// While downgraded, advertise the DELIVERED tier/detail (the true
|
||||
// stream) rather than the catalog max.
|
||||
tier: root.showDelivered ? QbzPlayer.npQualityEffectiveTier
|
||||
: QbzPlayer.npQualityTier
|
||||
detail: root.showDelivered ? QbzPlayer.npQualityTrueDetail
|
||||
: QbzPlayer.npQualityDetail
|
||||
}
|
||||
// Downgrade arrow (glyph, not a pill — ADR-008), amber so it reads as
|
||||
// "adjusted", not an error. Overlaid top-right so it never disturbs
|
||||
// the badge's width maths; the tooltip carries the cause.
|
||||
Text {
|
||||
visible: QbzPlayer.npQualityDowngraded
|
||||
text: "↓"
|
||||
font.pixelSize: 9
|
||||
font.weight: Font.Bold
|
||||
color: "#eab308"
|
||||
x: parent.width - width - 2
|
||||
y: 1
|
||||
}
|
||||
MouseArea {
|
||||
id: stampHover
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
ToolTip {
|
||||
id: stampTip
|
||||
visible: stampHover.containsMouse && root.qualityStampTooltip !== ""
|
||||
text: root.qualityStampTooltip
|
||||
delay: 0
|
||||
timeout: -1
|
||||
padding: 0
|
||||
x: Math.round((root.width - width) / 2)
|
||||
y: -height - 6
|
||||
contentItem: Text {
|
||||
text: stampTip.text
|
||||
color: theme.textSecondary
|
||||
font.pixelSize: 11
|
||||
font.weight: theme.weightMedium
|
||||
lineHeight: 1.25
|
||||
leftPadding: 9
|
||||
rightPadding: 9
|
||||
topPadding: 6
|
||||
bottomPadding: 6
|
||||
}
|
||||
background: Rectangle {
|
||||
color: theme.surfaceElevated
|
||||
radius: theme.radiusSm
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- the LED row (three mutually exclusive variants) -------------------
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 15
|
||||
|
||||
// A peer/cast device owns playback -> show the render TARGET instead
|
||||
// of the (now meaningless) local output LEDs.
|
||||
Row {
|
||||
visible: QbzPlayer.npIsRemote && QbzPlayer.npCastTarget !== ""
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
spacing: 4
|
||||
QbzIcon {
|
||||
name: "monitor-speaker"
|
||||
// Baked #e0b341 variant (assets/icons/amber/).
|
||||
tintName: "amber"
|
||||
width: 13
|
||||
height: 13
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
width: parent.width - 17
|
||||
text: QbzPlayer.npCastTarget
|
||||
color: "#e0b341"
|
||||
font.pixelSize: 9
|
||||
font.weight: theme.weightSemibold
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Local-network cast (Chromecast / DLNA), no QConnect peer.
|
||||
DotLed {
|
||||
visible: QbzPlayer.npCastActive && !QbzPlayer.npIsRemote
|
||||
label: QbzPlayer.npCastProtocol === "dlna" ? "DLNA" : "CAST"
|
||||
on: true
|
||||
onColor: "#a855f7"
|
||||
tooltip: QbzSession.tr("Casting", QbzSession.trRev)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
// Local output -> backend + mode dot LEDs, 8px apart.
|
||||
Row {
|
||||
visible: !QbzPlayer.npIsRemote && !QbzPlayer.npCastActive
|
||||
height: parent.height
|
||||
spacing: 8
|
||||
DotLed {
|
||||
label: QbzPlayer.npOutputBackendLabel
|
||||
on: QbzPlayer.npOutputBackendActive
|
||||
onColor: "#5b8def"
|
||||
tooltip: QbzSession.tr("Audio backend", QbzSession.trRev)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
DotLed {
|
||||
label: QbzPlayer.npOutputModeLabel
|
||||
on: QbzPlayer.npOutputModeActive
|
||||
onColor: "#22c55e"
|
||||
tooltip: QbzSession.tr("Output mode", QbzSession.trRev)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user