feat(purchases): readable dates, a release-date column and sort, column labels

The list printed the purchase date as "9/1/26" and nothing else, so a
row said neither when the record came out nor, at a glance, what the
number meant. Rows now carry the catalog's original release date next to
the purchase date, both as "Sep 1, 2026", under a column-label strip
like the album list's, and the toolbar sorts by release date (`released`)
beside the four existing keys.

cc
This commit is contained in:
vicrodh committed 2026-09-01 23:59:36 -06:00
1 parent 0756e96940
commit e41235f791
13 files changed
+209 -11

No files matched your search

@@ -39,6 +39,8 @@ Item {
readonly property int cardGap: 16
readonly property int listRowHeight: 60
readonly property int listGap: 4
/// The column-label strip above the rows (PurchaseListHeader), list arm only.
readonly property int listHeaderHeight: 28
readonly property int columns: Math.max(
1, Math.floor((width + root.cardGap) / (root.cardWidth + root.cardGap)))
@@ -49,7 +51,8 @@ Item {
// Content-sized: the page Flickable reads this through its Column.
height: root.viewMode === "list"
? (root.albums.length > 0
? root.albums.length * root.listRowHeight
? root.listHeaderHeight + root.listGap
+ root.albums.length * root.listRowHeight
+ (root.albums.length - 1) * root.listGap
: 0)
: (root.gridRows > 0
@@ -83,6 +86,11 @@ Item {
visible: root.viewMode === "list"
anchors.fill: parent
spacing: root.listGap
PurchaseListHeader {
visible: root.viewMode === "list" && root.albums.length > 0
width: parent.width
height: root.listHeaderHeight
}
Repeater {
model: root.viewMode === "list" ? root.albums : []
delegate: PurchaseListRow {
@@ -0,0 +1,49 @@
// PurchaseListHeader — the column labels above the purchases list rows.
// Widths mirror PurchaseListRow (art 48 · stretch · quality · released ·
// purchased, 12px gaps, 8/12 margins) so each label sits on its column, the
// way views/AlbumListHeader.qml does for the album list.
import QtQuick
import com.blitzfc.qbz
import "../../theme"
Item {
id: root
QbzTheme { id: theme }
function t(s) { return QbzSession.tr(s, QbzSession.trRev) }
readonly property int colQuality: 110
readonly property int colReleased: 120
readonly property int colPurchased: 130
width: parent ? parent.width : 0
height: 28
component ColLabel: Text {
color: theme.textMuted
font.pixelSize: 11
font.weight: theme.weightSemibold
font.letterSpacing: 0.5
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
height: parent.height
}
Row {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 12
spacing: 12
Item { width: 48; height: 1 }
ColLabel {
width: Math.max(0, parent.width - 48
- root.colQuality - root.colReleased - root.colPurchased - 4 * 12)
text: root.t("Title").toUpperCase()
}
ColLabel { width: root.colQuality; text: root.t("Quality").toUpperCase() }
ColLabel { width: root.colReleased; text: root.t("Release date").toUpperCase() }
ColLabel { width: root.colPurchased; text: root.t("Purchased").toUpperCase() }
}
}
@@ -33,8 +33,21 @@ Rectangle {
function t(s) { return QbzSession.tr(s, QbzSession.trRev) }
/// Epoch -> the viewer's short date. "" for a row that carries no stamp,
/// so the column collapses to blank rather than to 1970.
/// Column widths, shared with PurchaseListHeader so the labels sit on
/// their columns.
readonly property int colQuality: 110
readonly property int colReleased: 120
readonly property int colPurchased: 130
/// A date the way a person reads one ("Sep 1, 2026"), never the numeric
/// short form ("9/1/26") that the first port used: the two date columns
/// sit side by side and a reader has to tell them apart at a glance.
function readableDate(d) {
return Qt.formatDate(d, "MMM d, yyyy")
}
/// Epoch -> readable date. "" for a row that carries no stamp, so the
/// column collapses to blank rather than to 1970.
function purchaseDate() {
var ts = root.album.purchasedAt || 0
if (ts <= 0)
@@ -42,7 +55,20 @@ Rectangle {
// Defensive: the seam says seconds, but a producer that ever hands
// milliseconds through would otherwise print a date in the year 55000.
var ms = ts > 100000000000 ? ts : ts * 1000
return new Date(ms).toLocaleDateString(Qt.locale(), Locale.ShortFormat)
return root.readableDate(new Date(ms))
}
/// ISO `YYYY-MM-DD` -> readable date; a bare year stays a year; "" stays "".
function releaseDate() {
var iso = root.album.releaseDate || ""
if (iso === "")
return root.album.year || ""
var parts = iso.split("-")
if (parts.length < 3)
return parts[0]
// Local-time construction: `new Date("YYYY-MM-DD")` parses as UTC and
// prints the previous day west of Greenwich.
return root.readableDate(new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])))
}
width: parent ? parent.width : 0
@@ -95,9 +121,10 @@ Rectangle {
// Title over artist — the stretch column.
Column {
width: Math.max(0, parent.width - 48 - 110 - 110
width: Math.max(0, parent.width - 48
- root.colQuality - root.colReleased - root.colPurchased
- (unavailLabel.visible ? unavailLabel.width + 12 : 0)
- 3 * 12)
- 4 * 12)
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
@@ -120,24 +147,34 @@ Rectangle {
// Quality column — the SAME badge the album list rows show
// (views/AlbumListRow.qml): tier label over the exact quality line.
Item {
width: 110
width: root.colQuality
height: parent.height
QualityBadgeFull {
anchors.right: parent.right
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
tier: root.album.qualityTier || ""
detail: root.album.qualityDetail || ""
}
}
// Release-date column (the catalog's original release).
Text {
width: root.colReleased
height: parent.height
text: root.releaseDate()
color: theme.textSecondary
font.pixelSize: theme.fontLegal
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
// Purchase-date column.
Text {
width: 110
width: root.colPurchased
height: parent.height
text: root.purchaseDate()
color: theme.textMuted
font.pixelSize: theme.fontLegal
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
@@ -324,6 +324,7 @@ Item {
label: root.sortKey === "artist" ? root.t("Sort: Artist")
: root.sortKey === "album" ? root.t("Sort: Album")
: root.sortKey === "quality" ? root.t("Sort: Quality")
: root.sortKey === "released" ? root.t("Sort: Release date")
: root.t("Sort: Date")
onClicked: sortMenu.openBelowLeft(sortBtn)
@@ -352,6 +353,12 @@ Item {
arrow: sortBtn.arrowFor("purchased")
onPicked: sortBtn.pick("purchased")
}
MenuRow {
label: root.t("Release date")
selected: root.sortKey === "released"
arrow: sortBtn.arrowFor("released")
onPicked: sortBtn.pick("released")
}
MenuRow {
label: root.t("Artist")
selected: root.sortKey === "artist"