Files
qbz/crates/qbz-qt/qml/views/local/LocalAlbumRow.qml
T
vicrodh 81c43a114e fix(qt): glyph colours follow the theme, on light palettes and on accent fills
Two defects with one cause: a tint name that means a frozen hex.

"primary" is the legacy alias of literal #ffffff, not Theme.text-primary,
so every control passing it drew a white glyph. On the eleven light
themes that is white-on-light -- the header back/forward chevrons, the
sidebar toggle and the carousel arrows the owner marked. They now pass
the theme-following names, which icon_tint_qt bakes at runtime.

The stale `hslLightness > 0.5 ? "primary" : "black"` workarounds that
predate the runtime tinter collapse into those names, and the qrc floor
underneath them becomes polarity-aware: dir_for() returns "" whenever
the cache dir is unavailable, a bake fails or the theme document has not
been published, and that path used to resolve textPrimary straight back
to the white bake -- reproducing the bug it was fixing, app-wide, in the
degraded state.

On accent fills the fix is a deviation from Slint, approved by the owner
after measurement. A fixed white glyph on Theme.accent falls under 2.6:1
on 16 of the 35 shipped palettes, including high-contrast at 1.70:1 and
wcag-dark at 1.82:1 -- the two themes whose reason to exist is contrast.
Slint hardcodes #ffffff there (CircleAction.slint:72 and three others),
so the bug is upstream. accentText alone is not the answer either: it is
picked for the theme's text ramp and gives 2.56:1 on Rose Pine Dawn,
worse than the black it replaces. So QbzTheme picks the best-contrast
tint by real sRGB luminance against the live accent, and the labels
beside those glyphs use its colour twin -- BrowseGenreButton and
LibraryView were drawing a white glyph next to an accentText label
inside the same pill.

Rounds out the two parity items the same files owe: QbzNavButton gains
the ambient arm from Carousel.slint (wired at the six carousel sites,
not left dead), and QbzCircleAction rounds its glyph to whole pixels the
way CircleAction.slint does.

cc
2026-07-29 10:30:43 -06:00

204 lines
7.2 KiB
QML

// Compact album row for the LIST arm of the Albums / Folders-flat / Artists
// surfaces (AlbumCollectionView's list mode, mounted by
// LocalLibraryView.slint:1255 with show-source / show-source-badge on).
//
// 56px: cover, title + artist, year, track count, quality mark, the source
// column and the trailing ⋯ overflow. Multi-select puts the shared checkbox
// in front of the cover, as the collection view does.
//
// The ⋯ menu is AlbumListRow.slint:381 minus its two Qobuz-only entries: the
// favourite heart (local albums are not catalog favourites) and "Block this
// album" (which the Slint itself hides for source local/plex, :435). What is
// left is the five entries Slint always shows — Open album, Play, Play next,
// Play later, Add to queue — and all five are wired. Right-clicking the row
// opens the same menu at the pointer.
import QtQuick
import com.blitzfc.qbz
import "../../controls"
import "../../theme"
Rectangle {
id: root
property var item: ({})
property string artSource: ""
/// The LocalLibraryView root — the shared skeleton pulse, the per-item
/// artwork gate and the settle bound all live there (never duplicated
/// per row, so the rule cannot drift between surfaces).
property var view: null
property bool showSource: true
property bool selectMode: false
property bool checked: false
signal opened()
signal playRequested()
signal enqueueRequested(string mode)
signal toggleSelect()
QbzTheme { id: theme }
height: 56
radius: 6
color: rowArea.containsMouse ? theme.surfaceHover : "transparent"
MouseArea {
id: rowArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: function (mouse) {
if (mouse.button === Qt.RightButton) {
rowMenu.openAtCursor(rowArea, mouse.x, mouse.y)
return
}
if (root.selectMode) root.toggleSelect()
else root.opened()
}
onDoubleClicked: if (!root.selectMode) root.playRequested()
}
CardMenu {
id: rowMenu
menuWidth: 196
entries: [
{ "label": QbzSession.tr("Open album", QbzSession.trRev), "icon": "library-big", "action": "open" },
{ "label": QbzSession.tr("Play", QbzSession.trRev), "icon": "play-fill", "action": "play" },
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "action": "next" },
{ "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "action": "later" },
{ "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "action": "queue" },
]
onPicked: function (a) {
if (a === "open") root.opened()
else if (a === "play") root.playRequested()
else root.enqueueRequested(a)
}
}
Row {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 12
spacing: 12
Item {
visible: root.selectMode
width: visible ? 16 : 0
height: parent.height
SelectCheck {
anchors.centerIn: parent
on: root.checked
onToggled: root.toggleSelect()
}
}
Rectangle {
width: 40
height: 40
anchors.verticalCenter: parent.verticalCenter
radius: 4
color: theme.surfaceElevated
clip: true
RoundedImage {
id: rowArt
anchors.fill: parent
source: root.artSource
radius: 4
}
// Per-item: hands over the instant the art is actually ON the
// canvas (`rowArt.ready`, not "the path landed"); settles out
// when the album simply has none (local artwork drops such keys).
QbzSkeleton {
variant: "art"
anchors.fill: parent
blockRadius: 4
pending: root.view ? root.view.artWanted(root.item.artKey) : false
coverReady: rowArt.ready
phase: root.view ? root.view.skelPhase : false
settleMs: root.view ? root.view.artSettleMs : 0
settleHold: root.view ? root.view.artPulse : false
}
}
Column {
width: parent.width - 40 - 70 - 90 - 92 - 32
- (root.showSource ? 34 : 0)
- (root.selectMode ? 28 : 0) - 6 * 12
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
width: parent.width
text: root.item.title || ""
color: theme.textPrimary
font.pixelSize: 14
font.weight: theme.weightMedium
elide: Text.ElideRight
}
Text {
width: parent.width
text: root.item.artist || ""
color: theme.textMuted
font.pixelSize: 12
elide: Text.ElideRight
}
}
Text {
width: 70
anchors.verticalCenter: parent.verticalCenter
text: root.item.year || ""
color: theme.textMuted
font.pixelSize: 12
}
Text {
width: 90
anchors.verticalCenter: parent.verticalCenter
text: (root.item.trackCount || 0) + " "
+ QbzSession.tr("tracks", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: 12
}
Item {
width: 92
height: parent.height
QualityMini {
tier: root.item.qualityTier || ""
anchors.verticalCenter: parent.verticalCenter
}
}
Item {
visible: root.showSource
width: visible ? 34 : 0
height: parent.height
QbzIcon {
visible: root.item.source === "local" || root.item.source === "plex"
|| root.item.source === "offline"
name: root.item.source === "offline" ? "cloud-download" : "hard-drive"
width: 14
height: 14
anchors.verticalCenter: parent.verticalCenter
tintName: root.item.source === "plex" ? "accent" : "muted"
}
}
// Trailing ⋯ overflow (AlbumListRow.slint:360).
Rectangle {
width: 32
height: 32
radius: 6
anchors.verticalCenter: parent.verticalCenter
color: moreArea.containsMouse ? theme.surfaceElevated : "transparent"
QbzIcon {
name: "ellipsis"
width: 18
height: 18
anchors.centerIn: parent
tintName: moreArea.containsMouse ? "textPrimary" : "muted"
}
MouseArea {
id: moreArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: function (mouse) { rowMenu.openAtCursor(moreArea, mouse.x, mouse.y) }
}
}
}
}