Integrate the Kiosk profile hardening round: 12 dedicated Kiosk views plus 22 shared opt-in hosts, shared navigation history with Back/Forward, Albums as the default library tab, native paged Local Library (artists/tracks), lightweight MyQBZ grid and detail, Ephemeral lifecycle, bounded lists and artwork sized to the physical frame, static skeletons, 64px transport bar, Settings touch reflow and a persistent Immersive exit. Desktop keeps its prior behavior: every shared component defaults to the desktop path and only opts into Kiosk behavior through explicit properties (origin-relative ScrollMemory, viewport-bounded artwork requests). Regression coverage: kiosk navigation and artwork Node tests added to the runtime gate, scroll-origin fixtures for fixed and grouped rows, i18n strings for all eight locales. Verified by 750 release tests, six QML audits, Kiosk smokes at 800x480 and 1280x800 and a Desktop A/B with seven pixel-identical captures. cc
258 lines
10 KiB
QML
258 lines
10 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 used to be infinite scroll. Qt deliberately promotes that trigger to
|
|
// the shared explicit Load-more control: republishing a larger model
|
|
// from inside `onContentYChanged` changes the Flickable extent while it
|
|
// still carries momentum, which is the source of the tail rubber-band.
|
|
// :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).
|
|
//
|
|
// Back/forward scroll restore is handled by the shared ScrollMemory below;
|
|
// `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
|
|
property bool kioskHost: false
|
|
|
|
color: ambientOn ? "transparent" : theme.surfaceMain
|
|
readonly property bool ambientOn: theme.ambientOn
|
|
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: root.kioskHost ? 64 : 56
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
// Rounded at the TOP because this bar is full-bleed at y=0 of
|
|
// the content pane, and under the dynamic background the pane's
|
|
// own rounding cannot reach it: Qt's `clip` is a rectangular
|
|
// scissor that ignores `radius`, and AppShell hides its bezel
|
|
// nubs while the field is meant to show through the corners.
|
|
// So a full-bleed pane child rounds ITSELF — AppShell.qml says
|
|
// exactly this ("there is no mask that can do it for it here")
|
|
// and this bar was the counterexample the owner spotted in
|
|
// Discover. Invisible with the background off: the view root
|
|
// paints the same colour underneath.
|
|
topLeftRadius: theme.radiusMd
|
|
topRightRadius: theme.radiusMd
|
|
color: root.ambientOn ? theme.surfaceMainA30 : theme.surfaceMain
|
|
}
|
|
|
|
Text {
|
|
x: 48
|
|
y: (root.kioskHost ? 32 : 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: (root.kioskHost ? 32 : 25) - height / 2
|
|
spacing: 8
|
|
|
|
QbzLineEdit {
|
|
kioskHost: root.kioskHost
|
|
searchMode: true
|
|
width: 200
|
|
placeholder: QbzSession.tr("Search…", QbzSession.trRev)
|
|
text: root.query
|
|
onEdited: function (v) { QbzHome.discoverBrowseSearch(v) }
|
|
}
|
|
BrowseGenreButton {
|
|
btnHeight: root.kioskHost ? 44 : 34
|
|
context: "discover"
|
|
onClicked: genrePopup.toggle()
|
|
}
|
|
ViewModeToggle {
|
|
visible: !root.kioskHost
|
|
mode: root.viewMode
|
|
onSetMode: function (m) { QbzHome.discoverBrowseSetViewMode(m) }
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Scrolling list ----------------------------------------------
|
|
Item {
|
|
width: parent.width
|
|
height: parent.height - (root.kioskHost ? 64 : 56)
|
|
|
|
Flickable {
|
|
id: flick
|
|
anchors.fill: parent
|
|
clip: true
|
|
contentWidth: width
|
|
contentHeight: page.implicitHeight
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
|
|
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 {
|
|
kioskHost: root.kioskHost
|
|
id: collection
|
|
visible: !QbzHome.discoverBrowseLoading
|
|
width: parent.width - 64
|
|
collectionKey: root.doc.endpoint || ""
|
|
albums: root.items
|
|
viewMode: root.viewMode
|
|
cardWidth: 200
|
|
cardHeight: 266
|
|
cardGap: 24
|
|
listRowGap: 4
|
|
flick: flick
|
|
contentOffset: root.kioskHost ? y : (8)
|
|
}
|
|
|
|
Item {
|
|
visible: !QbzHome.discoverBrowseLoading
|
|
&& root.items.length > 0
|
|
&& root.doc.hasMore === true
|
|
&& root.query === ""
|
|
width: parent.width - 64
|
|
height: visible ? loadMore.height : 0
|
|
|
|
QbzLoadMore {
|
|
id: loadMore
|
|
width: parent.width
|
|
buttonHeight: root.kioskHost ? 64 : 32
|
|
busy: QbzHome.discoverBrowseLoadingMore
|
|
skeleton: root.viewMode === "list" ? "rows" : "cards"
|
|
// AlbumCollection pitch: 200x266 + 24px gutter.
|
|
cellW: 224
|
|
cellH: 290
|
|
rowH: 64
|
|
rowGap: 4
|
|
rowCount: 2
|
|
rowArtSize: 44
|
|
onClicked: {
|
|
collection.armTailFade()
|
|
QbzHome.discoverBrowseLoadMore()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tracks only the scrolling list (the header is a sibling above).
|
|
// Back/forward scroll memory (controls/ScrollMemory.qml): reports
|
|
// this container's offset while it is the live page, and restores it
|
|
// when a back/forward step arms this route.
|
|
ScrollMemory { target: flick; scope: "discoverbrowse" }
|
|
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
|
|
}
|
|
}
|