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
215 lines
7.6 KiB
QML
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
|
|
}
|
|
}
|
|
}
|