The mode LED is derived from SETTINGS (output_labels.rs reads audio.exclusive_mode and the ALSA plugin), so it reports what was ASKED FOR. Nothing reported what the opened stream actually negotiated, which means "exclusive worked" and "we asked for exclusive" were indistinguishable - and that distinction is what phase B's acceptance turns on. PlaybackEvent already carries bit_perfect_mode, so this is a publish, not new plumbing: the poll tick maps it to direct | plugin | disabled | unknown and set_effective_stream dedupes on it alongside the delivered params, so a steady stream still costs no Qt-thread hop. The quality tooltip appends "stream: <mode>" on both of its paths; unknown and empty print nothing. Four technical tokens, so no new translatable string. cc
193 lines
8.1 KiB
QML
193 lines
8.1 KiB
QML
// AudioStamp — 1:1 port of crates/qbz-ui/ui/shell/AudioStamp.slint.
|
|
//
|
|
// The inline 2-row now-playing stamp mounted by the SMALL and LARGE bars (the
|
|
// New and Classic bars mount the song card's QualityBadgeFull stamp instead):
|
|
//
|
|
// Row 1 (focus): [TIER] [↓] [detail] e.g. HI-RES 24-bit / 96 kHz
|
|
// Row 2 (secondary): [BACKEND] [MODE] the two LEDs, smaller, below
|
|
//
|
|
// Row 1 is the brighter line (detail = text-primary 9px/600, tier =
|
|
// text-secondary 9px/700) so the 7px LEDs never steal focus. No pills
|
|
// (ADR-008): the backend LED goes blue #5b8def when active, the mode LED green
|
|
// #3fae6a, both text-muted otherwise. Everything is right-aligned and clamped
|
|
// by `maxWidth` so it can never blow out the control row.
|
|
//
|
|
// Row 2 has three mutually exclusive variants, exactly as in the .slint:
|
|
// local — backend + mode LEDs (only while neither remote nor casting)
|
|
// cast — one purple #a855f7 "DLNA"/"CAST" LED
|
|
// remote — amber #e0b341 monitor-speaker icon + the QConnect renderer name
|
|
//
|
|
// While the stream is downgraded, row 1 reports the DELIVERED tier/detail with
|
|
// an amber #eab308 down arrow and the catalog max moves into the tooltip
|
|
// ("Source" / "Output" + the cause line).
|
|
//
|
|
// The tier label is built MANUALLY (hires→HI-RES, mp3→MP3, lossless→LOSSLESS,
|
|
// else CD) — this component deliberately does NOT use QualityBadgeFull, which
|
|
// carries the format icon and stacks tier over detail.
|
|
|
|
import QtQuick
|
|
import QtQuick.Controls
|
|
import com.blitzfc.qbz
|
|
import "../controls"
|
|
import "../theme"
|
|
|
|
Item {
|
|
id: root
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
/// Width clamp (Slint: the VerticalLayout's max-width at the call site).
|
|
property int maxWidth: 150
|
|
|
|
// Manual tier label: hires→HI-RES, mp3→MP3, lossless→LOSSLESS, else→CD.
|
|
readonly property string tierLabel: QbzPlayer.npQualityTier === "hires" ? "HI-RES"
|
|
: (QbzPlayer.npQualityTier === "mp3" ? "MP3"
|
|
: (QbzPlayer.npQualityTier === "lossless" ? "LOSSLESS" : "CD"))
|
|
|
|
// DELIVERED-first main line: while downgraded, row 1 reports what is
|
|
// actually playing and the catalog max moves to the tooltip's "Source".
|
|
readonly property bool showDelivered: QbzPlayer.npQualityDowngraded
|
|
&& QbzPlayer.npQualityTrueDetail !== ""
|
|
readonly property string deliveredTierLabel: QbzPlayer.npQualityEffectiveTier === "hires"
|
|
? "HI-RES"
|
|
: (QbzPlayer.npQualityEffectiveTier === "mp3" ? "MP3" : "CD")
|
|
|
|
// WHY the stream is below the catalog max, keyed by the QualityLimit
|
|
// discriminant. Informative, not alarming: a downgrade caused by the
|
|
// user's own setting is the app doing what it was told. Cause 3 only ever
|
|
// fires while casting, so it NAMES the renderer (its sibling cause 2 says
|
|
// "device" for the local DAC).
|
|
readonly property string causeLine: {
|
|
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 ""
|
|
}
|
|
|
|
// Tooltip for row 1: the catalog max ("Source") vs the truly delivered
|
|
// stream ("Output") plus the cause when known; the plain tier + detail
|
|
// otherwise. Empty when no detail exists (never a bare "CD: ").
|
|
readonly property string qualityTooltip: {
|
|
if (QbzPlayer.npQualityDetail === "") return ""
|
|
if (!showDelivered)
|
|
return tierLabel + ": " + QbzPlayer.npQualityDetail + root.streamModeLine
|
|
var t = QbzSession.tr("Source: {}", QbzSession.trRev).replace("{}", QbzPlayer.npQualityDetail)
|
|
+ "\n"
|
|
+ QbzSession.tr("Output: {}", QbzSession.trRev).replace("{}", QbzPlayer.npQualityTrueDetail)
|
|
t = causeLine === "" ? t : t + "\n" + causeLine
|
|
return t + root.streamModeLine
|
|
}
|
|
|
|
// What the OPENED stream negotiated, appended to the tooltip. The mode LED
|
|
// beside it is derived from SETTINGS, so it says what was ASKED FOR; this
|
|
// says what the engine actually got. When they disagree, that is the whole
|
|
// point. Four technical tokens, so nothing new to translate.
|
|
readonly property string streamModeLine: {
|
|
var m = QbzPlayer.npBitPerfectMode
|
|
if (m === "" || m === "unknown") return ""
|
|
return "\nstream: " + m
|
|
}
|
|
|
|
readonly property bool localOutput: !QbzPlayer.npIsRemote && !QbzPlayer.npCastActive
|
|
readonly property bool castOnly: QbzPlayer.npCastActive && !QbzPlayer.npIsRemote
|
|
readonly property bool remoteNamed: QbzPlayer.npIsRemote && QbzPlayer.npCastTarget !== ""
|
|
|
|
implicitWidth: Math.min(maxWidth, Math.max(qualityRow.implicitWidth,
|
|
ledRow.visible ? ledRow.implicitWidth : 0,
|
|
castRow.visible ? castRow.implicitWidth : 0,
|
|
remoteRow.visible ? remoteRow.implicitWidth : 0))
|
|
implicitHeight: stack.implicitHeight
|
|
width: implicitWidth
|
|
height: implicitHeight
|
|
|
|
Column {
|
|
id: stack
|
|
width: root.width
|
|
spacing: 1
|
|
|
|
// === Row 1 — quality, INLINE + BRIGHT (the focus) ================
|
|
// Shared with Album Quick View; AudioStamp retains ownership of the
|
|
// delivered/source decision and supplies only the resolved display.
|
|
QualityInline {
|
|
id: qualityRow
|
|
anchors.right: parent.right
|
|
maxWidth: root.maxWidth
|
|
tierLabel: root.showDelivered ? root.deliveredTierLabel : root.tierLabel
|
|
detail: root.showDelivered
|
|
? QbzPlayer.npQualityTrueDetail : QbzPlayer.npQualityDetail
|
|
downgraded: QbzPlayer.npQualityDowngraded
|
|
tooltipText: root.qualityTooltip
|
|
}
|
|
|
|
// === Row 2a — the LEDs (local output) ============================
|
|
Row {
|
|
id: ledRow
|
|
visible: root.localOutput
|
|
anchors.right: parent.right
|
|
spacing: 5
|
|
|
|
Text {
|
|
text: QbzPlayer.npOutputBackendLabel
|
|
font.pixelSize: 7
|
|
font.weight: Font.Bold
|
|
color: QbzPlayer.npOutputBackendActive ? "#5b8def" : theme.textMuted
|
|
elide: Text.ElideRight
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
Text {
|
|
text: QbzPlayer.npOutputModeLabel
|
|
font.pixelSize: 7
|
|
font.weight: Font.Bold
|
|
color: QbzPlayer.npOutputModeActive ? "#3fae6a" : theme.textMuted
|
|
elide: Text.ElideRight
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
|
|
// === Row 2b — local-network cast (Chromecast / DLNA) =============
|
|
Row {
|
|
id: castRow
|
|
visible: root.castOnly
|
|
anchors.right: parent.right
|
|
|
|
Text {
|
|
text: QbzPlayer.npCastProtocol === "dlna" ? "DLNA" : "CAST"
|
|
font.pixelSize: 7
|
|
font.weight: Font.Bold
|
|
color: "#a855f7"
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
|
|
// === Row 2c — QConnect renderer-name caption =====================
|
|
Row {
|
|
id: remoteRow
|
|
visible: root.remoteNamed
|
|
anchors.right: parent.right
|
|
spacing: 3
|
|
|
|
QbzIcon {
|
|
name: "monitor-speaker"
|
|
// Baked #e0b341 variant (assets/icons/amber/) — the software
|
|
// path cannot recolour at render time.
|
|
tintName: "amber"
|
|
width: 10
|
|
height: 10
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
Text {
|
|
text: QbzPlayer.npCastTarget
|
|
font.pixelSize: 8
|
|
font.weight: Font.DemiBold
|
|
color: "#e0b341"
|
|
elide: Text.ElideRight
|
|
width: Math.min(implicitWidth, 220)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
}
|
|
}
|