Files
qbz/crates/qbz-qt/qml/views/purchases/PurchaseGridCard.qml
T
vicrodh 2fc03d325d fix(qt): restore window lifecycle and improve grid artwork
Restore tray-aware close and quit confirmation, circular label covers, and missing label artist portraits. Publish artwork progressively, preload two grid rows, and skip redundant motion reports while preserving data refreshes.

Include regression tests and isolated artwork benchmarks. Verified Linux build, QML checks, and scrolling smoke tests.

cx
2026-09-09 21:58:25 -06:00

165 lines
6.7 KiB
QML

// PurchaseGridCard — one album cell of the Purchases grid. 1:1 port of
// purchases/PurchasesView.slint's `PurchaseGridCard` (:230-300).
//
// SELF-CONTAINED ON PURPOSE, and the .slint records the same call in its own
// header: the Purchases card is "card-lite" — cover / title / artist / quality,
// NO play overlay, no heart, no pin badge, no ⋯ menu — and it carries an
// UNAVAILABLE state that `cards/AlbumCard.qml` has no arm for at all.
// Threading two Purchases-only flags through the shared card would also have to
// go through `views/AlbumCollection.qml`, which mounts AlbumCard with ~15
// hardcoded properties, exposes no signals, and routes every click to
// `QbzAlbum.openAlbum` — i.e. straight to the CATALOG album page, which would
// leave the purchase-album route (§G.1: `QbzPurchases.openAlbum` then
// `QbzShell.navigateTo("purchase-album")`) unreachable. So the cell is its own
// file, exactly as the .slint decided for exactly these reasons.
//
// 162x232: a 1:1 cover (Radius.sm well, surface-elevated, disc-3 placeholder)
// over the title / artist / quality lines.
//
// UNAVAILABLE (`downloadable == false`): the cell dims to .45 (.55 on hover)
// and an alpha-60 scrim with the "Unavailable" label covers the artwork. The
// click is gated on it too — an unavailable purchase must not open a detail
// page that has nothing to offer. No `Behavior` on the opacity: it is fed by
// data and by hover, which is precisely the shape the repaint-pulse rule
// forbids animating (the .slint animates neither).
//
// There is deliberately NO "downloaded" mark here. Neither Tauri nor the
// .slint draws one on an album cell; `downloaded` feeds the "Hide downloaded"
// filter and the detail screen, and inventing a badge would be a behaviour
// this screen never had.
import QtQuick
import com.blitzfc.qbz
import "../../controls"
import "../../theme"
Item {
id: root
/// One row of `list_json.albums` (§G.2).
property var album: ({})
/// Viewport artwork resolver override. Empty falls back to the path that
/// was already cached when Rust built the document.
property string artSource: ""
signal clicked()
QbzTheme { id: theme }
readonly property bool avail: root.album.downloadable !== false
/// `artPath`, NEVER `artworkUrl`. The url is the REMOTE cover and this port
/// does not fetch covers from a delegate — Rust resolves them onto disk and
/// publishes the `file://` path, `""` until it lands. A cell handed only the
/// url draws nothing at all (purchases_qt.rs::AlbumRow::art_path, and the
/// same trap documented in artist_releases_qt.rs and musician_qt.rs).
readonly property string artUrl: root.artSource !== ""
? root.artSource : (root.album.artPath || "")
function t(s) { return QbzSession.tr(s, QbzSession.trRev) }
// .slint:238 — .45 / .55 on hover / 1.0.
opacity: root.avail ? 1.0 : (cardArea.containsMouse ? 0.55 : 0.45)
Column {
width: parent.width
spacing: 8
// --- Cover, 1:1 ---------------------------------------------------
Rectangle {
id: cover
width: parent.width
height: parent.width
radius: theme.radiusSm
color: theme.surfaceElevated
// No clip: RoundedImage confines its own crop and the scrim below
// is geometrically contained (the AlbumCard rule — one batch root
// per cell, no scissor that rounds nothing).
RoundedImage {
gridArtwork: true
id: art
anchors.fill: parent
source: root.artUrl
radius: theme.radiusSm
}
// .slint:252 — the empty-well glyph. Gated on `ready`, not on "the
// path is non-empty": a url that is still decoding (or that failed)
// must not leave a bare square (RoundedImage's readiness contract).
QbzIcon {
visible: !art.ready
name: "disc-3"
width: 36
height: 36
anchors.centerIn: parent
tintName: "muted"
}
// .slint:263-278 — the unavailable scrim over the artwork.
Rectangle {
visible: !root.avail
anchors.fill: parent
radius: theme.radiusSm
color: theme.alphaTier(60)
Text {
anchors.centerIn: parent
width: parent.width - 12
text: root.t("Unavailable")
color: theme.textPrimary
font.pixelSize: theme.fontLegal
font.weight: theme.weightSemibold
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
}
}
// --- Title / artist + quality badge ------------------------------
// The SAME icon-only badge every other album card draws
// (cards/AlbumCard.qml): tier glyph, exact quality in its tooltip.
// The .slint printed the raw tier word ("hires") as a third text
// line, which is the one thing on this page no other surface does.
Row {
width: parent.width
height: 40
spacing: theme.spacingSm
Column {
width: parent.width - (qBadge.visible ? qBadge.width + theme.spacingSm : 0)
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
width: parent.width
height: 20
text: root.album.title || ""
color: theme.textPrimary
font.pixelSize: theme.fontBody
font.weight: theme.weightMedium
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
Text {
width: parent.width
height: 17
text: root.album.artist || ""
color: theme.textMuted
font.pixelSize: theme.fontLegal
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
}
QualityMini {
id: qBadge
tier: root.album.qualityTier || ""
label: root.album.qualityDetail || ""
anchors.verticalCenter: parent.verticalCenter
}
}
}
MouseArea {
id: cardArea
anchors.fill: parent
hoverEnabled: true
cursorShape: root.avail ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: if (root.avail) root.clicked()
}
}