Files
qbz/crates/qbz-qt/qml/views/AwardAlbumsView.qml
T
vicrodh dc2496e5c0 feat(qt): award pages, reachable from the album sidebar and from each other
Ports the Slint/Tauri award pair: the landing (gold laurel hero, magazine
line, follow heart, "Acclaimed Releases" with its count and See All, the
preview grid and the "Other awards" rail) and the paginated listing with
its client-side search. Routes "award" and "awardalbums", the label pair's
shape — one controller for both, since they share the hero, the follow
state and the identity.

The three rules from the Tauri review are in the module header because they
are the ones a re-implementation gets wrong: the grid is ALWAYS
/award/getAlbums (/award/page is user-scoped and may omit releases, so it
feeds the hero and nothing else); the favorite split is plural-read,
singular-write, because the backend derives `award_ids` from the write
kind; and getAlbums has no has_more, its `total` is a hint, so the only
sound predicate is `total > loaded`.

The album sidebar's AWARDS cards navigate now — with an id directly, and by
NAME through the resolver when Qobuz omits it, which it does. They were
also reading `album.awards` when the field lives on `album.header`, so the
block had rendered EMPTY for every album since it was written: an empty
Repeater draws nothing and reports nothing, and the sidebar's own gate
counted the same empty list. The LABEL card beside it had a "no label view
yet" note and no handler, years after the label view landed.

Beyond the reference: an all-awards dropdown pinned to the page's top-right,
so any award is reachable without first finding an album that won it. It
costs nothing new — the /award/explore crawl already existed to resolve
names to ids.

The award icon was lucide's MICROPHONE under the wrong filename. Search had
routed around it with `crown` and left a note; kiosk was missed and drew a
mic for "Most popular". The asset is the laurel the other two frontends use,
and kiosk now asks for the crown it wanted.

cc
2026-08-16 18:56:20 -06:00

215 lines
7.6 KiB
QML

// Award "See all" listing — QML port of award/AwardAlbumsView.slint. Route
// "awardalbums".
//
// A search box over the LOADED set, the award name as the page title, and an
// infinite-scroll grid that asks for the next `/award/getAlbums` page as the
// tail comes into view. Same four exclusive body branches as the landing
// page, in the reference's order: loading · error+retry · no-results (a search
// that matched nothing) · the grid.
//
// The search is CLIENT-SIDE over what has been paginated in — 1:1 with the
// reference, which does not re-query for this box either. That is also why
// load-more is suppressed while a query is active: appending pages the user
// cannot see, to filter them out again, is work with no visible result.
//
// Every number, filter and page in the document comes from src/award_qt.rs;
// this file renders and reports scroll, nothing else.
import QtQuick
import com.blitzfc.qbz
import "../controls"
import "../theme"
Rectangle {
id: root
readonly property bool ambientOn: theme.ambientOn
color: root.ambientOn ? "transparent" : theme.surfaceMain
radius: 12
QbzTheme { id: theme }
readonly property var doc: {
try {
return JSON.parse(QbzHome.awardAlbumsJson)
} catch (e) {
return ({})
}
}
readonly property var albums: root.doc.albums || []
readonly property bool loading: QbzHome.awardAlbumsLoading === true
readonly property bool loadError: root.doc.loadError === true
readonly property string query: root.doc.searchQuery || ""
Flickable {
id: page
anchors.fill: parent
contentWidth: width
contentHeight: col.implicitHeight
boundsBehavior: Flickable.StopAtBounds
clip: true
// Infinite scroll. Gated on the SAME four conditions the reference
// gates on — not loading, not already loading more, there IS more, and
// no active query — so a filtered view never pages in behind it.
onContentYChanged: {
if (contentHeight - (contentY + height) < 600
&& root.doc.hasMore === true
&& root.doc.loadingMore !== true
&& !root.loading
&& root.query === "")
QbzHome.awardAlbumsLoadMore()
}
Column {
id: col
width: page.width - 64
x: 32
topPadding: 11
bottomPadding: 100
spacing: 0
Item { width: 1; height: 22 }
// ---- Search ------------------------------------------------------
QbzLineEdit {
width: 320
searchMode: true
placeholder: QbzSession.tr("Search albums & tracks…", QbzSession.trRev)
text: root.query
onEdited: function (v) { QbzHome.awardAlbumsSearch(v) }
}
Item { width: 1; height: 20 }
// ---- Title -------------------------------------------------------
Text {
width: parent.width
text: root.doc.name || ""
color: theme.textPrimary
font.pixelSize: theme.fontSection
font.weight: theme.weightBold
elide: Text.ElideRight
}
Item { width: 1; height: 4 }
Row {
spacing: 10
Text {
text: QbzSession.tr("Acclaimed Releases", QbzSession.trRev)
color: theme.textSecondary
font.pixelSize: theme.fontBody
}
Text {
visible: (root.doc.total || 0) > 0
text: root.doc.total || 0
color: theme.textMuted
font.pixelSize: theme.fontBody
font.weight: theme.weightMedium
}
}
Item { width: 1; height: 16 }
// ---- Body: loading / error+retry / no-results / grid ------------
Item {
visible: root.loading
width: parent.width
height: visible ? 280 : 0
Column {
anchors.centerIn: parent
spacing: 18
QbzSpinner {
anchors.horizontalCenter: parent.horizontalCenter
size: 36
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: QbzSession.tr("Loading…", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: 13
}
}
}
Item {
visible: !root.loading && root.loadError
width: parent.width
height: visible ? 240 : 0
Column {
anchors.centerIn: parent
spacing: 16
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: QbzSession.tr("Failed to load Library", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: 14
}
SettingsButton {
anchors.horizontalCenter: parent.horizontalCenter
text: QbzSession.tr("Retry", QbzSession.trRev)
onClicked: QbzHome.awardRetry()
}
}
}
// A search that matched nothing is NOT the same as an award with
// no releases, and the reference keeps them as two strings.
Item {
visible: !root.loading && !root.loadError
&& root.albums.length === 0 && root.query !== ""
width: parent.width
height: visible ? 200 : 0
Text {
anchors.centerIn: parent
text: QbzSession.tr("No results found.", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: 14
}
}
Item {
visible: !root.loading && !root.loadError
&& root.albums.length === 0 && root.query === ""
width: parent.width
height: visible ? 200 : 0
Text {
anchors.centerIn: parent
text: QbzSession.tr("No award-winning releases yet.", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: 14
}
}
AlbumCollection {
visible: !root.loading && !root.loadError && root.albums.length > 0
width: parent.width
collectionKey: root.doc.id || ""
albums: root.albums
viewMode: "grid"
isGrouped: false
cardWidth: 200
cardHeight: 266
cardGap: 24
}
// Tail spinner for the next page.
Item {
visible: root.doc.loadingMore === true
width: parent.width
height: visible ? 72 : 0
QbzSpinner {
anchors.centerIn: parent
size: 28
}
}
}
QbzScrollBar {
target: page
anchors.right: parent.right
anchors.rightMargin: 4
anchors.top: parent.top
anchors.bottom: parent.bottom
}
}
}