**The scaled-cover fix from the last round produced nothing.** The seam ran — 462 requests in one offscreen smoke — and wrote zero files, so the owner was right that nothing improved. The artwork cache stores files as `.img`, and `image::open` picks its decoder from the EXTENSION, so every call failed silently. Sniffing the content instead yields 101 derivatives on the same run. The header atmosphere had the identical bug and had therefore never produced an image either, which is why the flat fallback was all anyone ever saw. Reported as fixed last round without checking that it ran. Verified by effect this time, not by reading the diff. The rest came through an analyse / refute / implement / verify pipeline, and the adversarial passes earned their keep: - The sidebar mini rail: icons sat left of centre because their slot kept its fixed 24px width while the rail collapsed, so the hit target was rectangular and off-axis. The rail narrows, rows are square, and the mini state gains the folder flyout and tooltips it never had. The reviewer caught a comment claiming the width change was a one-token revert when it is two — corrected rather than left to mislead the next reader. - Playback context now travels from every producer, not the handful that happened to stamp it. - Collapsible per-tab search in Library, and label artwork. - Every View-all destination Home offers, plus the label page and its releases. - Collapsing the sidebar drops a Large bar back to New, live, without overwriting the user's persisted preference. cc
223 lines
8.0 KiB
QML
223 lines
8.0 KiB
QML
// Discover "View all" full-list page — QML port of
|
|
// discover/DiscoverBrowseView.slint. One /discover/<module> endpoint (New
|
|
// Releases, Press Accolades, Ideal Discography, Qobuzissimes, Albums of the
|
|
// Week, Most Streamed) as a paginated full grid or list.
|
|
//
|
|
// Layout, read off the .slint:
|
|
// :44 fixed 56px header — title LEFT, search / genre / grid-list RIGHT
|
|
// :139 body padding 32 / 32, top 8, bottom 100
|
|
// :26 200x266 cards, gap 24; :29 64px list rows, gap 4
|
|
// :126 infinite scroll: within 600px of the bottom, has-more, not already
|
|
// loading, and NO active search (the filter is client-side, so paging
|
|
// under one would bring back unfiltered rows)
|
|
// :205 scrollbar below the header; :233 genre popup anchored y=56
|
|
//
|
|
// NAV BUTTONS: the .slint draws a NavButtons pair at x=32 and starts the
|
|
// title 16px after it. In this port back/forward live in the shell header
|
|
// (HeaderBar.qml:235), so the title starts at x=48 — the 32 + 0 + 16 the
|
|
// HomeView toolbar already uses (HomeView.qml:744).
|
|
//
|
|
// POC-NOTEs: no scroll restore (the port has no counterpart to
|
|
// NavState.report-scroll / restore-scope); `windowed` grid mounting is the
|
|
// AlbumCollection sampler rather than the .slint's own.
|
|
|
|
import QtQuick
|
|
import QtQuick.Window
|
|
import com.blitzfc.qbz
|
|
import "../controls"
|
|
import "../theme"
|
|
|
|
Rectangle {
|
|
id: root
|
|
|
|
color: ambientOn ? "transparent" : theme.surfaceMain
|
|
readonly property bool ambientOn: QbzShell.ambientMode > 0 && QbzPlayer.npHasTrack
|
|
radius: 12
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
readonly property var doc: {
|
|
try {
|
|
return JSON.parse(QbzHome.discoverBrowseJson)
|
|
} catch (e) {
|
|
return {}
|
|
}
|
|
}
|
|
readonly property var items: doc.items || []
|
|
readonly property string query: doc.query || ""
|
|
// Seeded to "grid" by the Rust opener — an empty mode renders nothing.
|
|
readonly property string viewMode: doc.viewMode || "grid"
|
|
|
|
// --- Shared genre selection ------------------------------------------
|
|
// `genre_filter_qt::after_selection_change` only knows how to reload
|
|
// Home, so this page watches the published selection itself and asks for
|
|
// a re-fetch from offset 0. The signature is the selected NAMES, not the
|
|
// count: swapping one genre for another leaves the count unchanged.
|
|
readonly property string genreSig: {
|
|
try {
|
|
var d = JSON.parse(QbzBridge.genreFilterJson)
|
|
return JSON.stringify((d.names || {})["discover"] || [])
|
|
} catch (e) {
|
|
return ""
|
|
}
|
|
}
|
|
property string lastGenreSig: ""
|
|
onGenreSigChanged: {
|
|
if (root.lastGenreSig !== "" && root.lastGenreSig !== root.genreSig)
|
|
QbzHome.discoverBrowseGenreChanged()
|
|
root.lastGenreSig = root.genreSig
|
|
}
|
|
Component.onCompleted: root.lastGenreSig = root.genreSig
|
|
|
|
// ============================ offline gate ============================
|
|
QbzOfflinePlaceholder {
|
|
visible: QbzSession.offline
|
|
anchors.centerIn: parent
|
|
showSettingsAction: true
|
|
onSettingsClicked: QbzShell.navigateTo("settings")
|
|
}
|
|
|
|
Column {
|
|
anchors.fill: parent
|
|
spacing: 0
|
|
visible: !QbzSession.offline
|
|
|
|
// --- Fixed 56px header -------------------------------------------
|
|
Item {
|
|
id: header
|
|
width: parent.width
|
|
height: 56
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: root.ambientOn ? theme.surfaceMainA30 : theme.surfaceMain
|
|
}
|
|
|
|
Text {
|
|
x: 48
|
|
y: 25 - height / 2
|
|
width: Math.max(0, tools.x - 48 - 16)
|
|
text: root.doc.title || ""
|
|
color: theme.textPrimary
|
|
font.pixelSize: theme.fontSection
|
|
font.weight: theme.weightBold
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
Row {
|
|
id: tools
|
|
x: parent.width - width - 32
|
|
y: 25 - height / 2
|
|
spacing: 8
|
|
|
|
QbzLineEdit {
|
|
searchMode: true
|
|
width: 200
|
|
placeholder: QbzSession.tr("Search…", QbzSession.trRev)
|
|
text: root.query
|
|
onEdited: function (v) { QbzHome.discoverBrowseSearch(v) }
|
|
}
|
|
BrowseGenreButton {
|
|
context: "discover"
|
|
onClicked: genrePopup.toggle()
|
|
}
|
|
ViewModeToggle {
|
|
mode: root.viewMode
|
|
onSetMode: function (m) { QbzHome.discoverBrowseSetViewMode(m) }
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Scrolling list ----------------------------------------------
|
|
Item {
|
|
width: parent.width
|
|
height: parent.height - 56
|
|
|
|
Flickable {
|
|
id: flick
|
|
anchors.fill: parent
|
|
clip: true
|
|
contentWidth: width
|
|
contentHeight: page.implicitHeight
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
|
|
// Infinite scroll, guarded exactly as the .slint is.
|
|
onContentYChanged: {
|
|
if (contentY + height >= contentHeight - 600
|
|
&& root.doc.hasMore === true
|
|
&& !QbzHome.discoverBrowseLoadingMore
|
|
&& !QbzHome.discoverBrowseLoading
|
|
&& root.query === "")
|
|
QbzHome.discoverBrowseLoadMore()
|
|
}
|
|
|
|
Column {
|
|
id: page
|
|
width: parent.width
|
|
leftPadding: 32
|
|
rightPadding: 32
|
|
topPadding: 8
|
|
bottomPadding: 100
|
|
spacing: 0
|
|
|
|
QbzSpinner {
|
|
visible: QbzHome.discoverBrowseLoading
|
|
size: 36
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
}
|
|
|
|
// Search filtered everything out.
|
|
Text {
|
|
visible: !QbzHome.discoverBrowseLoading
|
|
&& root.items.length === 0 && root.query !== ""
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
text: QbzSession.tr("No results found.", QbzSession.trRev)
|
|
color: theme.textMuted
|
|
font.pixelSize: 14
|
|
}
|
|
|
|
AlbumCollection {
|
|
visible: !QbzHome.discoverBrowseLoading
|
|
width: parent.width - 64
|
|
albums: root.items
|
|
viewMode: root.viewMode
|
|
cardWidth: 200
|
|
cardHeight: 266
|
|
cardGap: 24
|
|
listRowGap: 4
|
|
flick: flick
|
|
contentOffset: 8
|
|
}
|
|
|
|
Item { visible: QbzHome.discoverBrowseLoadingMore; width: 1; height: 24 }
|
|
QbzSpinner {
|
|
visible: QbzHome.discoverBrowseLoadingMore
|
|
size: 28
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tracks only the scrolling list (the header is a sibling above).
|
|
QbzScrollBar {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 4
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
target: flick
|
|
}
|
|
}
|
|
}
|
|
|
|
// Declared LAST — declaration order IS z-order, so the popup keeps its
|
|
// own presses instead of the toolbar swallowing them.
|
|
GenreFilterPopup {
|
|
id: genrePopup
|
|
anchors.fill: parent
|
|
context: "discover"
|
|
// The .slint anchors this page's popup at y=56 (not HomeView's 52).
|
|
anchorTop: 56
|
|
anchorRight: 32
|
|
}
|
|
}
|