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
221 lines
9.4 KiB
QML
221 lines
9.4 KiB
QML
// TrackCard — THE track card (discover/TrackCard.slint), promoted from
|
|
// LibraryView.LibTrackCard in phase 21: Library Tracks grid + All feed.
|
|
// 200x246: 200px cover + hover scrim 0.6, overlay row (heart / play /
|
|
// more) at y=120, optional source badge, then the meta row: title +
|
|
// "Track • Artist" (artist links out) + the icon-only quality badge.
|
|
// Body click PLAYS (the TrackCard convention, unlike album/playlist).
|
|
//
|
|
// item contract: { id, title, artist, artistId, albumId, qualityTier,
|
|
// isFavorite, source } plus the host-resolved artSource string prop.
|
|
//
|
|
// NOTE the Search most-popular track hero is NOT this card (Slint:
|
|
// primitives/SearchTrackHero.slint — a distinct 160x220 filled-chrome
|
|
// hero); the POC keeps its own SearchTrackHero variant (200x246, centered
|
|
// play, quality as text) in SearchView.qml with a justifying comment.
|
|
|
|
import QtQuick
|
|
import com.blitzfc.qbz
|
|
import "../controls"
|
|
import "../theme"
|
|
|
|
Rectangle {
|
|
id: root
|
|
|
|
property var item: ({})
|
|
// Host-resolved artwork path (the AlbumCard artSource pattern).
|
|
property string artSource: ""
|
|
// Remote cover URL for the pin payload ("" when the host has none).
|
|
property string artworkUrl: ""
|
|
// ADR-008 source glyph (local/Plex) — the Library All-feed arm.
|
|
property bool showSourceBadge: false
|
|
|
|
color: "transparent"
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
readonly property bool overlayOn: tcArtArea.containsMouse || favBtn.hovered
|
|
|| playBtn.hovered || moreBtn.hovered
|
|
|
|
implicitWidth: 200
|
|
implicitHeight: 246
|
|
|
|
Column {
|
|
spacing: 0
|
|
Rectangle {
|
|
width: 200
|
|
height: 200
|
|
radius: theme.radiusSm
|
|
color: theme.surfaceElevated
|
|
clip: true
|
|
RoundedImage {
|
|
anchors.fill: parent
|
|
source: root.artSource
|
|
radius: theme.radiusSm
|
|
}
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: theme.radiusSm
|
|
color: "#000000"
|
|
opacity: root.overlayOn ? 0.6 : 0.0
|
|
Behavior on opacity { NumberAnimation { duration: 150 } }
|
|
}
|
|
// Body click PLAYS the track (TrackCard hover); right press opens
|
|
// the SAME menu as the ⋯ button.
|
|
MouseArea {
|
|
id: tcArtArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
onClicked: function (mouse) {
|
|
if (mouse.button === Qt.RightButton)
|
|
trackMenu.openAtCursor(tcArtArea, mouse.x, mouse.y)
|
|
else
|
|
QbzPlayer.playTrack(root.item.id)
|
|
}
|
|
}
|
|
// Hover overlay — favorite / play / more (y=120, h=44,
|
|
// centered, spacing 12).
|
|
CardOverlayRow {
|
|
y: 120
|
|
width: parent.width
|
|
shown: root.overlayOn
|
|
CardOverlayButton {
|
|
id: favBtn
|
|
name: root.item.isFavorite ? "heart-filled" : "heart"
|
|
active: root.item.isFavorite
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: {
|
|
root.item.isFavorite = !root.item.isFavorite
|
|
QbzLibrary.libraryToggleFavorite("track", root.item.id)
|
|
}
|
|
}
|
|
CardOverlayButton {
|
|
id: playBtn
|
|
name: "play-fill"
|
|
primary: true
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: QbzPlayer.playTrack(root.item.id)
|
|
}
|
|
CardOverlayButton {
|
|
id: moreBtn
|
|
name: "ellipsis"
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: function (mouse) { trackMenu.openAtCursor(moreBtn, mouse.x, mouse.y) }
|
|
}
|
|
}
|
|
// Source badge (All feed, show-local): bottom-right.
|
|
Rectangle {
|
|
visible: root.showSourceBadge && (root.item.source === "local" || root.item.source === "plex")
|
|
x: parent.width - width - 6
|
|
y: parent.height - height - 6
|
|
width: 24
|
|
height: 24
|
|
radius: 4
|
|
color: "#b3000000"
|
|
QbzIcon {
|
|
name: "hard-drive"
|
|
width: 14
|
|
height: 14
|
|
anchors.centerIn: parent
|
|
// On the #b3000000 badge — dark under every theme.
|
|
tintName: root.item.source === "plex" ? "accent" : "white"
|
|
}
|
|
}
|
|
CardMenu {
|
|
id: trackMenu
|
|
menuWidth: 196
|
|
entries: root.menuModel()
|
|
onPicked: function (a) { root.trackAction(a) }
|
|
}
|
|
}
|
|
Item { width: 1; height: 6 }
|
|
// Title / "Track • Artist" + quality badge.
|
|
Row {
|
|
width: 200
|
|
height: 40
|
|
spacing: theme.spacingSm
|
|
Column {
|
|
width: parent.width - (tcQ.visible ? tcQ.width + theme.spacingSm : 0)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 2
|
|
Text {
|
|
width: parent.width
|
|
height: 20
|
|
text: root.item.title || ""
|
|
color: tcTitleArea.containsMouse ? theme.accent : theme.textPrimary
|
|
font.pixelSize: theme.fontBody - 2
|
|
font.weight: theme.weightMedium
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
MouseArea {
|
|
id: tcTitleArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
onClicked: function (mouse) {
|
|
if (mouse.button === Qt.RightButton)
|
|
trackMenu.openAtCursor(tcTitleArea, mouse.x, mouse.y)
|
|
else
|
|
QbzPlayer.playTrack(root.item.id)
|
|
}
|
|
}
|
|
}
|
|
Text {
|
|
width: parent.width
|
|
height: 18
|
|
text: QbzSession.tr("Track", QbzSession.trRev) + " • " + (root.item.artist || "")
|
|
color: root.item.artistId && tcArtistArea.containsMouse
|
|
? theme.textPrimary : theme.textMuted
|
|
font.pixelSize: theme.fontLink - 1
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
MouseArea {
|
|
id: tcArtistArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: root.item.artistId ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
onClicked: if (root.item.artistId) QbzArtist.openArtist(root.item.artistId)
|
|
}
|
|
}
|
|
}
|
|
QualityMini { id: tcQ; tier: root.item.qualityTier || ""; anchors.verticalCenter: parent.verticalCenter }
|
|
}
|
|
}
|
|
|
|
// Track context-menu model (TrackCard.slint track-menu) + dispatch.
|
|
// COMPLETE against the .slint: Play · Play next · Play later (#442, end
|
|
// of the manual block) · Add to queue · Go to artist (artist-id != "") ·
|
|
// Go to album (album-id != "") · favorite. The card menu is deliberately
|
|
// shorter than the row menu — the .slint card has no radio / share /
|
|
// offline / track-info rows.
|
|
function menuModel() {
|
|
var t = QbzSession.tr
|
|
var r = QbzSession.trRev
|
|
var m = [
|
|
{ "label": t("Play", r), "icon": "play-fill", "action": "play" },
|
|
{ "label": t("Play next", r), "icon": "list-start", "action": "next" },
|
|
{ "label": t("Play later", r), "icon": "list-plus", "action": "later" },
|
|
{ "label": t("Add to queue", r), "icon": "list-end", "action": "queue" },
|
|
]
|
|
if (root.item.artistId) m.push({ "label": t("Go to artist", r), "icon": "user", "action": "go-artist" })
|
|
if (root.item.albumId) m.push({ "label": t("Go to album", r), "icon": "disc", "action": "go-album" })
|
|
m.push({ "label": root.item.isFavorite ? t("Remove from Library", r) : t("Add to Library", r),
|
|
"icon": root.item.isFavorite ? "heart-filled" : "heart", "action": "favorite" })
|
|
return m
|
|
}
|
|
function trackAction(a) {
|
|
if (a === "play") QbzPlayer.playTrack(root.item.id)
|
|
else if (a === "next") QbzPlayer.enqueueTrack(root.item.id, "next")
|
|
else if (a === "later") QbzPlayer.enqueueTrack(root.item.id, "later")
|
|
else if (a === "queue") QbzPlayer.enqueueTrack(root.item.id, "queue")
|
|
else if (a === "go-artist") QbzArtist.openArtist(root.item.artistId)
|
|
else if (a === "go-album") QbzAlbum.openAlbum(root.item.albumId)
|
|
else if (a === "favorite") {
|
|
root.item.isFavorite = !root.item.isFavorite
|
|
QbzLibrary.libraryToggleFavorite("track", root.item.id)
|
|
}
|
|
}
|
|
}
|