feat(qbz-qt): theme-following icons, the real header atmosphere, sharp covers, playback context

**Icons follow the theme.** The bakes are frozen dark-palette hexes, so on light
themes secondary, muted and accent were simply wrong, and accent was wrong on
any theme whose accent is not #4285f4. Slint recolours at render time, which
this path cannot do — so the tint is generated once per colour into the cache
and loaded through the same Image+svg path the bakes already use. No effect, no
shader, so it cannot hit the "renders nothing on software" failure.

The sweep found the part the diagnosis had missed: "primary" was two meanings
welded together — max-contrast-on-theme-surface, and literal white for glyphs on
hosts that are dark under every theme. Remapping it globally would have painted
dark glyphs on dark scrims in fourteen places. The vocabulary is split instead,
with "primary" left as an alias so nothing changes until it is migrated
deliberately.

**The album header gradient was Slint's fallback, not its design.** Slint paints
a blurred artwork field and only falls back to the flat two-band gradient when
that image is unavailable; the port shipped the fallback. The field needs no
shader — it is a 128x128 image, so the pipeline ports verbatim: the cover down
to 8x8, back up to 128, blur, colour adjust, vignette. The 8x8 step is the look,
not an optimization; everything after it works from eight pixels of colour,
which is why it reads as atmosphere rather than as a blurred cover. It comes out
of the same decode that already produced the flat tint.

**Covers were point-sampled.** Canvas drawImage does not filter when it scales,
measured at RMSE 0.000 against a nearest-neighbour reference — a 600px cover in
a 200px cell throws away eight of every nine pixels, and the error is aliasing,
not softness. Rust now produces a derivative at the drawn device size so the
draw is 1:1 and there is no resample at all.

**The now-playing context stack.** It had no Qt counterpart: the model carried
context_kind and context_id, some builders stamped them, and nothing ever read
them back. The album path only appeared to work by coincidence, because the
glyph was hardcoded to open the album.

Plus: Discover's missing rails with real play history, Recommendations on the
integrations, queue drag reorder, and the playlist detail collage.

cc
This commit is contained in:
vicrodh committed 2026-07-28 21:12:31 -06:00
1 parent a1c771c39e
commit 2a5b5ac16e
31 files changed
+3548 -329

No files matched your search

+19 -7
View File
@@ -138,7 +138,11 @@ Rectangle {
width: navTab.showIcon ? 14 : 0
height: 14
anchors.verticalCenter: parent.verticalCenter
tintName: navTab.isActive ? "primary" : "secondary"
// Both arms are THEME tokens (the tab sits on surface-elevated
// / surface-hover): "textPrimary", never the fixed-white
// "primary" bake, or the active tab is white-on-white on every
// light theme. See the vocabulary in QbzIcon.qml.
tintName: navTab.isActive ? "textPrimary" : "secondary"
}
Text {
height: parent.height
@@ -193,7 +197,7 @@ Rectangle {
width: 16
height: 16
anchors.centerIn: parent
tintName: cnb.isActive ? "primary" : "secondary"
tintName: cnb.isActive ? "textPrimary" : "secondary"
}
MouseArea {
id: cnbArea
@@ -308,7 +312,7 @@ Rectangle {
width: 16
height: 16
anchors.centerIn: parent
tintName: plPopup.opened ? "primary" : "secondary"
tintName: plPopup.opened ? "textPrimary" : "secondary"
}
MouseArea {
id: plBtnArea
@@ -579,11 +583,15 @@ Rectangle {
}
}
}
// Placeholder (centered when empty + unfocused, left once typing).
// Placeholder — centered on the WHOLE box, exactly like
// HeaderBar.slint:792 (`width: parent.width; horizontal-alignment:
// center`). The magnifier is absolutely positioned at x:10 and takes
// no layout space, so it must not shift the placeholder either: the
// 30px left inset this used to carry pushed "Search" 15px right of
// the field's centre, which is what reads as "not centred".
Text {
visible: searchInput.text === "" && !searchInput.activeFocus
anchors.fill: parent
anchors.leftMargin: 30
text: QbzSession.tr("Search", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: 13
@@ -617,7 +625,7 @@ Rectangle {
width: 12
height: 12
anchors.centerIn: parent
tintName: clearArea.containsMouse ? "primary" : "muted"
tintName: clearArea.containsMouse ? "textPrimary" : "muted"
}
MouseArea {
id: clearArea
@@ -835,7 +843,11 @@ Rectangle {
width: 14
height: 14
anchors.centerIn: parent
tintName: wcCloseArea.containsMouse ? "primary" : "secondary"
// The hover fill is a FIXED #e81123 under every theme, so the
// hovered glyph is literal white ("white"), not a theme
// token — the one place in this file where the fixed bake is
// the right answer.
tintName: wcCloseArea.containsMouse ? "white" : "secondary"
}
MouseArea {
id: wcCloseArea
+202 -23
View File
@@ -4,15 +4,14 @@
// Tabs (Queue / History), NOW PLAYING card (live heart), UP NEXT with the
// #442 "Next in queue" / "Next up" section markers, 40-row pagination,
// live search filter, row actions (play / remove / remove-all-after /
// heart), drag reorder (basic press-drag), footer (count line + Clear /
// stubs + search field), History tab (thumbnail rows, click replays as a
// fresh single-track queue), exact empty states. Data: QbzQueue.queueJson
// (queue_qt.rs QueueDoc).
// heart), drag reorder (6px press-drag, drop indicator + floating ghost),
// footer (count line + Clear / stubs + search field), History tab
// (thumbnail rows, click replays as a fresh single-track queue), exact
// empty states. Data: QbzQueue.queueJson (queue_qt.rs QueueDoc).
//
// POC-NOTEs: save-as-playlist (opens a picker modal upstream — inert),
// infinite-play + sleep timer engines, stop-after marker, row menu's
// "Add to playlist" / "Track info" entries, ephemeral rows, the ghost
// drag pill (rows move with a plain displaced animation instead).
// "Add to playlist" / "Track info" entries, ephemeral rows.
import QtQuick
import QtQuick.Controls
@@ -37,11 +36,52 @@ Rectangle {
// url-keyed cover map (shared artwork pipeline).
property var coverMap: ({})
// Drag-reorder state (basic press-drag; ghost is the row's own y).
property int dragFrom: -1
// --- UP NEXT drag-reorder state (QueueSidebar.slint:371-410) ---------
// Panel-local on purpose: this is NOT the QbzShell drag global (that one
// is the track -> sidebar-playlist ghost, and its drag_end() posts an
// "add tracks to playlist" — a reorder must never reach it).
// ALL indices here are PAGE-LOCAL, which is exactly what queue_qt.rs's
// move_track takes (it resolves page-local -> queue-wide itself, filter
// and pagination included) — same contract as play/remove/remove-after.
property bool reorderActive: false
property int reorderFrom: -1 // page-local index of the dragged row
property int reorderOver: -1 // insertion slot, 0..N
property real reorderPointerY: 0 // live pointer WINDOW y (drives the ghost)
QbzTheme { id: theme }
// Pointer window-Y -> insertion slot [0..N]. The Slint does this with a
// fixed 44px stride plus a 22px correction per #442 section header; here
// the rows' real geometry answers it directly (the delegate exposes its
// row center in window space), so section headers, the list padding and
// the 8px inter-row spacing are all accounted for without a second
// arithmetic model to keep in sync. Top half of row k -> slot k, bottom
// half -> slot k+1, past the last row -> N (append). <=40 rows per page.
function slotFromPointer(winY) {
var n = root.upcoming.length
for (var i = 0; i < n; i++) {
var d = upcomingRepeater.itemAt(i)
if (d && winY < d.rowCenterY())
return i
}
return n
}
// Commit — QueueSidebar.slint commit-reorder(). `to == from` and
// `to == from + 1` both drop back onto the SAME gap, so neither reaches
// the bridge (move_track's from == to early return would swallow them
// anyway, at the cost of a queue round-trip and a republish).
function commitReorder() {
var from = root.reorderFrom
var to = root.reorderOver
root.reorderActive = false
root.reorderFrom = -1
root.reorderOver = -1
if (from < 0 || to < 0 || to === from || to === from + 1)
return
QbzQueue.queueMoveTrack(from, to)
}
Connections {
target: QbzLibrary
function onLibraryArtworkReady(key, path) {
@@ -122,21 +162,35 @@ Rectangle {
// One UP NEXT / History row (QueueRow.slint).
component QueueRow: Rectangle {
id: qrRoot
property var row: ({})
property int rowIndex: 0
property bool showNumber: true
property bool inQueue: true
/// Drag-reorder source? UP NEXT rows only — History has no order to
/// commit, and it keeps its drag-scroll because of this (see qrArea's
/// preventStealing).
property bool reorderable: false
readonly property bool hovered: qrArea.containsMouse || menuArea.containsMouse
readonly property bool isActive: inQueue && QbzPlayer.npTrackId !== "" && QbzPlayer.npTrackId === row.id
readonly property bool dragSource: reorderable && root.reorderActive
&& root.reorderFrom === rowIndex
// Row center in WINDOW space — what slotFromPointer() measures the
// pointer against. A function, not a binding: mapToItem() is not
// reactive, and it is only ever read during a live drag.
function rowCenterY() {
return qrRoot.mapToItem(null, 0, qrRoot.height / 2).y
}
height: 44
radius: theme.radiusSm
color: hovered ? theme.surfaceHover
: (rowIndex % 2 === 1 ? "#592a2a2a" : "transparent")
border.width: root.dragFrom === rowIndex ? 1 : 0
border.width: dragSource ? 1 : 0
border.color: theme.accent
opacity: root.dragFrom === rowIndex ? 0.4 : 1.0
opacity: dragSource ? 0.4 : 1.0
Row {
anchors.fill: parent
@@ -301,16 +355,115 @@ Rectangle {
}
}
// Row body: click plays, >6px press-drag reorders (QueueSidebar
// .slint's `ta` does both from one TouchArea — one area per row, not
// two stacked).
MouseArea {
id: qrArea
anchors.fill: parent
// BEHIND the row's own controls. Declaration order IS z-order in
// QML and this fills the row from the last declaration, so at the
// default z it sat on top of the ⋯ button and swallowed its press
// — the menu never opened, the row just played. Same fix (and the
// same reason the .slint declares its TouchArea BEFORE the
// content) as rows/TrackRow.qml's trArea.
z: -1
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
// THE reason drag-reorder scrolled instead of reordering: the
// enclosing Flickable filters its children's mouse events and
// grabs the press once the pointer moves past
// QStyleHints.startDragDistance (~10px), after which this area
// gets `canceled`, never `released`. Keeping the grab here is the
// Qt equivalent of QueueSidebar.slint's `interactive: false` on
// the queue Flickable — but scoped to the row and without that
// side effect: Qt's Flickable also drops WHEEL events when it is
// non-interactive, so wheel + scrollbar keep working, and only a
// press that starts ON an UP NEXT row is denied to the flick.
// (Consequence, same as the Slint: no drag-scroll while
// reordering — pagination covers long lists.)
preventStealing: qrRoot.reorderable
property real downY: 0
property bool dragging: false
// A release that ended a drag must NOT also play the track
// (`clicked` fires after `released`).
property bool suppressClick: false
onPressed: function (mouse) {
qrArea.downY = qrArea.mapToItem(null, mouse.x, mouse.y).y
qrArea.dragging = false
}
onPositionChanged: function (mouse) {
if (!qrRoot.reorderable || !qrArea.pressed)
return
// WINDOW space for both the threshold and the slot, so a
// wheel-scroll mid-gesture cannot corrupt either.
var winY = qrArea.mapToItem(null, mouse.x, mouse.y).y
if (!qrArea.dragging && Math.abs(winY - qrArea.downY) > 6) {
qrArea.dragging = true
root.reorderActive = true
root.reorderFrom = qrRoot.rowIndex
root.reorderOver = qrRoot.rowIndex
}
if (qrArea.dragging) {
root.reorderPointerY = winY
root.reorderOver = root.slotFromPointer(winY)
}
}
onReleased: {
if (!qrArea.dragging)
return
qrArea.dragging = false
qrArea.suppressClick = true
root.commitReorder()
}
// Safety net (the .slint keeps one too): if the grab is lost
// mid-drag the release never arrives here, and without this the
// panel would stay stuck in reorderActive with a ghost pinned to
// the last pointer position.
onCanceled: {
if (!qrArea.dragging)
return
qrArea.dragging = false
qrArea.suppressClick = true
root.commitReorder()
}
onClicked: {
if (qrArea.suppressClick) {
qrArea.suppressClick = false
return
}
if (inQueue) QbzQueue.queuePlayUpcoming(rowIndex)
else QbzQueue.queuePlayHistory(rowIndex)
}
}
// Drop indicator — the .slint draws one 2px accent line at the
// insertion slot, hidden on the two no-op slots (from, from+1). Here
// it rides the row that OWNS the slot (its top edge, and the last
// row's bottom edge for the append slot), which needs no overlay
// geometry and stays correct across the section headers.
Rectangle {
visible: qrRoot.reorderable && root.reorderActive
&& root.reorderOver === qrRoot.rowIndex
&& root.reorderOver !== root.reorderFrom
&& root.reorderOver !== root.reorderFrom + 1
y: -1
width: parent.width
height: 2
radius: 1
color: theme.accent
}
Rectangle {
visible: qrRoot.reorderable && root.reorderActive
&& root.reorderOver === root.upcoming.length
&& qrRoot.rowIndex === root.upcoming.length - 1
&& root.reorderFrom !== root.upcoming.length - 1
y: parent.height - 1
width: parent.width
height: 2
radius: 1
color: theme.accent
}
}
Column {
@@ -511,11 +664,16 @@ Rectangle {
}
Repeater {
id: upcomingRepeater
model: root.upcoming
delegate: Column {
id: upDelegate
required property var modelData
required property int index
width: parent ? parent.width : 0
// Row center in window space — slotFromPointer()
// reads it off the Repeater's items.
function rowCenterY() { return qrow.rowCenterY() }
// #442 section header above the row.
Row {
visible: modelData.section !== ""
@@ -540,24 +698,13 @@ Rectangle {
}
}
QueueRow {
id: qrow
width: parent.width
row: modelData
rowIndex: index
showNumber: true
inQueue: true
// Basic press-drag reorder (POC: no ghost pill).
MouseArea {
anchors.fill: parent
onPressAndHold: root.dragFrom = index
onReleased: {
if (root.dragFrom >= 0) {
var target = Math.max(0, Math.min(root.upcoming.length - 1,
Math.floor((parent.mapToItem(null, mouseX, mouseY).y - queueBody.mapToItem(null, 0, 0).y - 40) / 44)))
QbzQueue.queueMoveTrack((doc.page || 0) * 40 + root.dragFrom, (doc.page || 0) * 40 + target)
root.dragFrom = -1
}
}
}
reorderable: true
}
}
}
@@ -736,4 +883,36 @@ Rectangle {
}
}
}
// Floating drag ghost (QueueSidebar.slint:1098-1130) — a pill following
// the pointer with the dragged track's title, so it is clear something is
// being moved. A free child of the panel ROOT (not of the Column, which
// would stack it), declared LAST so it renders above the list, and with
// no mouse target of its own. The live pointer window-Y is converted into
// panel-local space here; mapToItem() is re-evaluated on every
// reorderPointerY change, which is the only thing that moves it.
Rectangle {
visible: root.reorderActive && root.reorderFrom >= 0
&& root.reorderFrom < root.upcoming.length
x: theme.spacingMd
y: root.reorderPointerY - root.mapToItem(null, 0, 0).y - height / 2
width: root.width - 2 * theme.spacingMd
height: 34
radius: theme.radiusSm
color: theme.surfaceElevated
border.width: 1
border.color: theme.accent
opacity: 0.95
Text {
anchors.fill: parent
anchors.leftMargin: theme.spacingSm
anchors.rightMargin: theme.spacingSm
verticalAlignment: Text.AlignVCenter
text: parent.visible ? (root.upcoming[root.reorderFrom].title || "") : ""
color: theme.textPrimary
font.pixelSize: 12
font.weight: theme.weightMedium
elide: Text.ElideRight
}
}
}
+187 -62
View File
@@ -6,11 +6,21 @@
// surface-elevated fill); New uses the flush 74px variant. Large mounts it
// with showArt/showBadges false — the cover lives in the sidebar dock and the
// AudioStamp moves to the right cluster, so the text column reclaims the
// stamp's 132px.
// stamp's 132px. `textCenter` is the SMALL-bar variant (SongCard.slint:329,
// mounted by PlayerBarSmall.slint:239-251): a 37px bordered cover, a 13px
// title and the title+meta collapsed into a tight centred group, no in-card
// stamp.
//
// The title click opens Track Info (SongCard.slint: `root.track-info()` from
// the title TouchArea, Qobuz-only) — the host bar owns the modal, this just
// emits `trackInfoRequested()`.
//
// META ROW (SongCard.slint:20-49 `MetaLink` + :486-540): [layers] artist · album.
// The two labels share the leftover width on the Slint's stretch budget —
// artist 2, album 1 — each capped at its natural width and free to shrink to
// nothing. A plain Row of unbounded Texts is what let the album slide past the
// card wall and vanish under the audio stamp / the bar column's `clip: true`
// (PlayerBar.qml:249): `elide` does NOTHING on a Text with no explicit width.
import QtQuick
import com.blitzfc.qbz
@@ -23,6 +33,18 @@ Rectangle {
property bool showBadges: true
property bool glass: false
property int artSize: glass ? 60 : 74
// Optional cover border (SongCard.slint art-border-width / -color). OFF by
// default so New/Classic/Large are untouched; Small passes 3px surface-card.
property int artBorderWidth: 0
property color artBorderColor: "transparent"
// Cover -> text gap (SongCard.slint art-text-gap: 11px default, 9px Small).
property int artTextGap: 11
// Title size (SongCard.slint title-font-size: Typography.body default,
// 13px on the Small bar).
property real titleFontSize: theme.fontBody
// Small bar: title+meta as a TIGHT centred group instead of the
// full-height, space-distributed column.
property bool textCenter: false
/// Title clicked — the host opens the Track Info modal.
signal trackInfoRequested()
@@ -35,6 +57,34 @@ Rectangle {
readonly property bool qobuzTrack: QbzPlayer.npHasTrack
&& /^[0-9]+$/.test(QbzPlayer.npTrackId)
// "Playing from" origin, 1:1 with SongCard.slint:151-156: the CURRENT
// track's own stamped container, falling back to its album when it carries
// none (the Slint fallback in playback.rs:1959-1965).
readonly property string ctxKind: QbzPlayer.npContextId !== ""
? QbzPlayer.npContextKind : "album"
readonly property string ctxId: QbzPlayer.npContextId !== ""
? QbzPlayer.npContextId : QbzPlayer.npAlbumId
// SongCard.slint fires `open-context(kind, id)`, which the bar routes to
// `media-action(kind, id, "open")` — qbz/src/main.rs:12695 (artist), :12701
// (album), :12707 (playlist), :13206 (label).
function openContext() {
if (root.ctxId === "")
return
if (root.ctxKind === "artist") {
QbzArtist.openArtist(root.ctxId)
} else if (root.ctxKind === "playlist") {
QbzBridge.openPlaylist(root.ctxId)
} else if (root.ctxKind === "album") {
QbzAlbum.openAlbum(root.ctxId)
} else if (QbzPlayer.npAlbumId !== "") {
// "label" (and anything added later) has no landing page in this
// port — LabelCard.qml documents the same gap. Fall back to the
// track's album so the glyph is never a dead control.
QbzAlbum.openAlbum(QbzPlayer.npAlbumId)
}
}
QbzTheme { id: theme }
// Bake matching the LIVE Theme.text-primary (SongCard.slint:504 tints the
@@ -52,8 +102,8 @@ Rectangle {
Row {
anchors.left: parent.left
// SongCard.slint's inner-row width formula: the card minus the
// stamp + a 4px gap ONLY when the stamp is rendered (Large hides
// it — the AudioStamp moves to the right column — so the text
// stamp + a 4px gap ONLY when the stamp is rendered (Large and Small
// hide it — the AudioStamp moves to the right column — so the text
// column must reclaim those 132px).
anchors.right: stamp.visible ? stamp.left : parent.right
anchors.rightMargin: stamp.visible ? 4 : 0
@@ -70,6 +120,8 @@ Rectangle {
anchors.centerIn: parent
radius: 6
color: theme.surfaceElevated
border.width: root.artBorderWidth
border.color: root.artBorderColor
clip: true
RoundedImage {
visible: QbzPlayer.npHasTrack
@@ -106,18 +158,21 @@ Rectangle {
}
}
}
Item { width: root.showArt ? 11 : 0; height: 1 }
Item { width: root.showArt ? root.artTextGap : 0; height: 1 }
Column {
width: parent.width - (root.showArt ? root.artSize + 11 : 0)
width: parent.width - (root.showArt ? root.artSize + root.artTextGap : 0)
anchors.verticalCenter: parent.verticalCenter
spacing: 4
// textCenter tightens the inter-line gap to 1px so the two lines
// read as one block beside the small cover (SongCard.slint:333);
// the default column keeps its 4px.
spacing: root.textCenter ? 1 : 4
Text {
width: parent.width
text: QbzPlayer.npHasTrack ? QbzPlayer.npTitle
: QbzSession.tr("Nothing playing", QbzSession.trRev)
color: theme.textPrimary
font.pixelSize: theme.fontBody
font.pixelSize: root.titleFontSize
font.weight: theme.weightMedium
elide: Text.ElideRight
// Title -> Track Info (SongCard.slint). The MouseArea is a
@@ -130,65 +185,135 @@ Rectangle {
onClicked: if (root.qobuzTrack) root.trackInfoRequested()
}
}
Row {
// --- Meta row: [layers] artist · album ------------------------
Item {
id: metaRow
visible: QbzPlayer.npHasTrack
spacing: 7
width: parent.width
// Row height is driven by the 16x18 context box (SongCard.slint).
height: 18
// Context-stack icon ("Show track playing context" pref).
Rectangle {
visible: QbzPlayer.showContextIcon
width: 16
height: 18
radius: 3
color: ctxArea.containsMouse ? theme.surfaceHover : "transparent"
QbzIcon {
name: "layers"
width: 13
height: 13
// Natural (unelided) label widths. `implicitWidth` on a Text
// that already HAS an explicit width is not a safe source here
// — TextMetrics measures the string itself and never feeds the
// width binding back into the layout.
TextMetrics {
id: artistMetrics
font: artistText.font
text: artistText.text
}
TextMetrics {
id: albumMetrics
font: albumText.font
text: albumText.text
}
// Fixed furniture: the context box (+ its 7px gap when shown)
// and the separator dot with a 7px gap on each side.
readonly property real fixedW: (ctxBox.visible ? 16 + 7 : 0) + 3 + 14
readonly property real avail: Math.max(0, metaRow.width - metaRow.fixedW)
// Slint stretch 2 (artist) : 1 (album), each capped at its own
// natural width and free to shrink to 0 (SongCard.slint:22-31):
// a long album elides before the artist does, and both keep a
// readable slice instead of one pushing the other off the card.
readonly property var budget: {
var a = artistMetrics.width
var b = albumMetrics.width
var av = metaRow.avail
if (a + b <= av)
return [a, b]
var qa = av * 2 / 3
var qb = av - qa
if (a < qa) {
qa = a
qb = av - a
} else if (b < qb) {
qb = b
qa = av - b
}
return [Math.floor(qa), Math.floor(qb)]
}
readonly property bool artistLinked: QbzPlayer.npArtistId !== ""
readonly property bool albumLinked: QbzPlayer.npAlbumId !== ""
Row {
anchors.fill: parent
spacing: 7
// Context-stack icon ("Show track playing context" pref) —
// opens the container playback was launched from.
Rectangle {
id: ctxBox
visible: QbzPlayer.showContextIcon
width: 16
height: 18
radius: 3
color: ctxArea.containsMouse ? theme.surfaceHover : "transparent"
QbzIcon {
name: "layers"
width: 13
height: 13
// Flush LEFT (SongCard.slint pins x: 0) so the meta
// line starts on the title's column.
x: 0
anchors.verticalCenter: parent.verticalCenter
tintName: ctxArea.containsMouse ? root.tintStrong : "muted"
}
MouseArea {
id: ctxArea
anchors.fill: parent
hoverEnabled: true
cursorShape: root.ctxId !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: root.openContext()
}
}
Text {
id: artistText
width: metaRow.budget[0]
height: parent.height
verticalAlignment: Text.AlignVCenter
text: QbzPlayer.npArtist
color: artistArea.containsMouse && metaRow.artistLinked
? theme.textPrimary : theme.textMuted
font.pixelSize: 11
elide: Text.ElideRight
MouseArea {
id: artistArea
anchors.fill: parent
hoverEnabled: true
cursorShape: metaRow.artistLinked ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: if (metaRow.artistLinked) QbzArtist.openArtist(QbzPlayer.npArtistId)
}
}
Rectangle {
width: 3
height: 3
radius: 1.5
color: theme.textMuted
anchors.verticalCenter: parent.verticalCenter
tintName: ctxArea.containsMouse ? root.tintStrong : "muted"
}
MouseArea {
id: ctxArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: if (QbzPlayer.npAlbumId !== "") QbzAlbum.openAlbum(QbzPlayer.npAlbumId)
}
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: QbzPlayer.npArtist
color: artistArea.containsMouse && QbzPlayer.npArtistId !== "" ? theme.accent : theme.textMuted
font.pixelSize: 12
elide: Text.ElideRight
MouseArea {
id: artistArea
anchors.fill: parent
hoverEnabled: true
cursorShape: QbzPlayer.npArtistId !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: if (QbzPlayer.npArtistId !== "") QbzArtist.openArtist(QbzPlayer.npArtistId)
}
}
Rectangle {
width: 3
height: 3
radius: 1.5
color: theme.textMuted
anchors.verticalCenter: parent.verticalCenter
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: QbzPlayer.npAlbum
color: albumArea.containsMouse && QbzPlayer.npAlbumId !== "" ? theme.accent : theme.textMuted
font.pixelSize: 12
elide: Text.ElideRight
MouseArea {
id: albumArea
anchors.fill: parent
hoverEnabled: true
cursorShape: QbzPlayer.npAlbumId !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: if (QbzPlayer.npAlbumId !== "") QbzAlbum.openAlbum(QbzPlayer.npAlbumId)
Text {
id: albumText
width: metaRow.budget[1]
height: parent.height
verticalAlignment: Text.AlignVCenter
text: QbzPlayer.npAlbum
color: albumArea.containsMouse && metaRow.albumLinked
? theme.textPrimary : theme.textMuted
font.pixelSize: 11
elide: Text.ElideRight
MouseArea {
id: albumArea
anchors.fill: parent
hoverEnabled: true
cursorShape: metaRow.albumLinked ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: if (metaRow.albumLinked) QbzAlbum.openAlbum(QbzPlayer.npAlbumId)
}
}
}
}