// 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 "../../theme" Item { id: root /// One row of `list_json.albums` (§G.2). property var album: ({}) 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.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 { 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 ------------------------------------ Column { width: parent.width 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 } // .slint:288 — the quality line is dropped entirely when the row // carries no tier, rather than reserving an empty band. Text { visible: (root.album.qualityTier || "") !== "" width: parent.width height: visible ? 17 : 0 text: root.album.qualityTier || "" color: theme.textSecondary font.pixelSize: theme.fontLegal verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } } } MouseArea { id: cardArea anchors.fill: parent hoverEnabled: true cursorShape: root.avail ? Qt.PointingHandCursor : Qt.ArrowCursor onClicked: if (root.avail) root.clicked() } }