219 lines
9.3 KiB
QML
219 lines
9.3 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
|
|
/// AppShell's shared topmost tooltip. The Small/Large desktop bars pass it
|
|
/// through so the bubble is outside every clipped player-bar subtree.
|
|
property Item tooltipHost: null
|
|
|
|
// Manual tier label: hires→HI-RES, mp3→MP3, lossless→LOSSLESS, else→CD.
|
|
readonly property string tierLabel: QbzPlayer.npQualityTier === "hires" ? "HI-RES"
|
|
: (QbzPlayer.npQualityTier === "dsd" ? "DSD"
|
|
: (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 === "dsd" ? "DSD"
|
|
: (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 {
|
|
id: backendLed
|
|
text: QbzPlayer.npOutputBackendLabel
|
|
font.pixelSize: 7
|
|
font.weight: Font.Bold
|
|
color: QbzPlayer.npOutputBackendActive ? "#5b8def" : theme.textMuted
|
|
elide: Text.ElideRight
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onContainsMouseChanged: {
|
|
if (root.tooltipHost)
|
|
root.tooltipHost.hover(containsMouse, backendLed,
|
|
"npb-backend", QbzPlayer.npOutputBackendTooltip)
|
|
}
|
|
}
|
|
}
|
|
Text {
|
|
id: modeLed
|
|
text: QbzPlayer.npOutputModeLabel
|
|
font.pixelSize: 7
|
|
font.weight: Font.Bold
|
|
color: QbzPlayer.npOutputModeActive ? "#3fae6a" : theme.textMuted
|
|
elide: Text.ElideRight
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onContainsMouseChanged: {
|
|
if (root.tooltipHost)
|
|
root.tooltipHost.hover(containsMouse, modeLed,
|
|
"npb-output-mode",
|
|
QbzSession.tr("Output mode", QbzSession.trRev))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// === 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
|
|
}
|
|
}
|
|
}
|
|
}
|