224 lines
8.5 KiB
QML
224 lines
8.5 KiB
QML
// PurchaseTrackRow — one row of the Purchases TRACKS tab. 1:1 port of
|
|
// purchases/PurchasesView.slint's `PurchaseTrackRow` (:400-560).
|
|
//
|
|
// NOT rows/TrackRow.qml. The .slint's own header names the reason and it holds
|
|
// here too: this row's columns are Purchases-only (a BARE quality string, a
|
|
// purchase-date column, a download slot) and it has neither the number/play
|
|
// cell, nor the heart, nor the ⋯ menu that the shared row is built around —
|
|
// §15.1 explicitly forbids a track context menu on this screen. It also has no
|
|
// column header above it, so `rows/TrackCols.qml`'s whole reason to exist (a
|
|
// header and a row that cannot disagree) does not apply.
|
|
//
|
|
// 56px, Radius.sm: 40x40 art · title over "artist · album" · a 64px quality
|
|
// column · a 44px duration column · a 100px purchase-date column · a 30px
|
|
// download slot.
|
|
//
|
|
// STATE, and one deliberate reduction from the .slint. The reference's download
|
|
// slot is a FOUR-state control (idle / downloading / complete / failed) that
|
|
// opens an anchored format picker. §G carries no picker seam — `list_json`
|
|
// tracks publish `downloaded` and nothing else about a download, and §G.1 has
|
|
// no picker invokable — so the slot here is a read-only INDICATOR: a success
|
|
// check on a downloaded row, an empty reserve otherwise. The column geometry is
|
|
// the reference's, so the control can be armed later without moving a pixel.
|
|
// (Flagged to the orchestrator rather than invented.)
|
|
//
|
|
// The row body plays, and only when `streamable` — the purchases-list default
|
|
// is TRUE (§G.2), so an unplayable row is one the API really did mark that way.
|
|
|
|
import QtQuick
|
|
import com.blitzfc.qbz
|
|
import "../../theme"
|
|
|
|
Rectangle {
|
|
id: root
|
|
|
|
/// One row of `list_json.tracks` (§G.2).
|
|
property var track: ({})
|
|
property string artSource: ""
|
|
/// Row index — the odd-row zebra every other track list draws.
|
|
property int rowIndex: 0
|
|
signal playRequested()
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
readonly property bool streamable: root.track.streamable !== false
|
|
readonly property bool downloaded: root.track.downloaded === true
|
|
/// The now-playing row, read from the shared player state — the .slint's
|
|
/// `PurchasesState.active-track-id` has no equivalent in `list_json`, and
|
|
/// `QbzPlayer.npTrackId` is what every other row in this port asks
|
|
/// (rows/TrackRow.qml:324).
|
|
readonly property bool active: QbzPlayer.npTrackId === (root.track.id || "")
|
|
&& (root.track.id || "") !== ""
|
|
|
|
/// §10-C / §G.3: `title (version)`. Published empty today on the list
|
|
/// document, so the call is inert until the producer fills it — which is
|
|
/// exactly why it must be here and not hard-coded away.
|
|
function displayTitle() {
|
|
var ttl = root.track.title || ""
|
|
var ver = root.track.version || ""
|
|
return ver !== "" ? ttl + " (" + ver + ")" : ttl
|
|
}
|
|
|
|
function fmtDuration(secs) {
|
|
var s = Math.max(0, Math.floor(secs || 0))
|
|
var m = Math.floor(s / 60)
|
|
var r = s % 60
|
|
return m + ":" + (r < 10 ? "0" : "") + r
|
|
}
|
|
|
|
function purchaseDate() {
|
|
var ts = root.track.purchasedAt || 0
|
|
if (ts <= 0)
|
|
return ""
|
|
var ms = ts > 100000000000 ? ts : ts * 1000
|
|
return new Date(ms).toLocaleDateString(Qt.locale(), Locale.ShortFormat)
|
|
}
|
|
|
|
width: parent ? parent.width : 0
|
|
height: 56
|
|
radius: theme.radiusSm
|
|
// .slint:414 — the active row takes alpha-10; hover only applies to a row
|
|
// the click can actually act on.
|
|
color: root.active ? theme.alphaTier(10)
|
|
: ((rowArea.containsMouse && root.streamable) ? theme.surfaceHover
|
|
: (root.rowIndex % 2 === 1 ? theme.alphaTier(4) : "transparent"))
|
|
// .slint:418 — a downloaded row reads as "already dealt with".
|
|
opacity: root.downloaded ? 0.75 : 1.0
|
|
|
|
// Declared FIRST so the cells that want their own pointer (none today, but
|
|
// the download slot lands here) are hit-tested after it, exactly like the
|
|
// .slint's ordering note.
|
|
MouseArea {
|
|
id: rowArea
|
|
anchors.fill: parent
|
|
enabled: root.streamable
|
|
hoverEnabled: true
|
|
cursorShape: root.streamable ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
onClicked: root.playRequested()
|
|
}
|
|
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 8
|
|
anchors.rightMargin: 12
|
|
spacing: 12
|
|
|
|
// 40x40 artwork.
|
|
Item {
|
|
width: 40
|
|
height: parent.height
|
|
Rectangle {
|
|
width: 40
|
|
height: 40
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
radius: theme.radiusSm
|
|
color: theme.surfaceElevated
|
|
RoundedImage {
|
|
id: art
|
|
anchors.fill: parent
|
|
// `artPath`, never `artworkUrl` — see PurchaseGridCard.
|
|
source: root.artSource !== "" ? root.artSource
|
|
: (root.track.artPath || "")
|
|
radius: theme.radiusSm
|
|
}
|
|
QbzIcon {
|
|
visible: !art.ready
|
|
name: "music"
|
|
width: 16
|
|
height: 16
|
|
anchors.centerIn: parent
|
|
tintName: "muted"
|
|
}
|
|
}
|
|
}
|
|
|
|
// Title over the "artist · album" meta line — the stretch column.
|
|
Column {
|
|
width: Math.max(0, parent.width - 40
|
|
- (qualityCell.visible ? 64 + 12 : 0)
|
|
- 44 - 100 - 30 - 4 * 12)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 2
|
|
Text {
|
|
width: parent.width
|
|
text: root.displayTitle()
|
|
color: root.active ? theme.accent : theme.textPrimary
|
|
font.pixelSize: theme.fontBody
|
|
font.weight: theme.weightMedium
|
|
elide: Text.ElideRight
|
|
}
|
|
// `artist · album`. The .slint makes the artist a link; the list
|
|
// document carries no `artistId` (§G.2), so there is no page to
|
|
// route to and a pointer that promises one would lie. Plain text
|
|
// until the seam grows the field.
|
|
Text {
|
|
width: parent.width
|
|
text: {
|
|
var a = root.track.artist || ""
|
|
var al = root.track.album || ""
|
|
return al !== "" ? (a + " · " + al) : a
|
|
}
|
|
color: theme.textMuted
|
|
font.pixelSize: theme.fontLegal
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
|
|
// Quality column — BARE, and dropped entirely when the row has none
|
|
// (.slint:498).
|
|
Text {
|
|
id: qualityCell
|
|
visible: (root.track.qualityTier || "") !== ""
|
|
width: 64
|
|
height: parent.height
|
|
text: root.track.qualityTier || ""
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontLegal
|
|
horizontalAlignment: Text.AlignRight
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
// Duration.
|
|
Text {
|
|
width: 44
|
|
height: parent.height
|
|
text: root.fmtDuration(root.track.duration)
|
|
color: theme.textMuted
|
|
font.pixelSize: theme.fontLegal
|
|
horizontalAlignment: Text.AlignRight
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
|
|
// Purchase date.
|
|
Text {
|
|
width: 100
|
|
height: parent.height
|
|
text: root.purchaseDate()
|
|
color: theme.textMuted
|
|
font.pixelSize: theme.fontLegal
|
|
horizontalAlignment: Text.AlignRight
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
// Download slot — see the header: the reference's 4-state control,
|
|
// reduced to its "complete" arm because §G publishes no other state.
|
|
Item {
|
|
width: 30
|
|
height: parent.height
|
|
QbzIcon {
|
|
visible: root.downloaded
|
|
name: "check"
|
|
width: 16
|
|
height: 16
|
|
anchors.centerIn: parent
|
|
// The .slint paints this one `Theme.success`; QbzIcon's tint
|
|
// vocabulary has no success arm, and "accent" is the token this
|
|
// port uses for a positive glyph on a theme surface.
|
|
tintName: "accent"
|
|
}
|
|
}
|
|
}
|
|
}
|