Address the owner's kiosk feedback on top of the release hardening: - Chrome: the back bar is a drag surface and shrank to 44px with one shared button form for Back/Forward/Connect/Cast; Qobuz Connect and Cast are a permanent cluster in the bar; the dead Share-logs button left the kiosk Settings header. - Transport bar: the hidden overflow menu is gone; Volume is always visible with a vertical sheet, and Desktop mode has its own button. - Now Playing: one centred transport row (New-bar order); lyrics are centred (shared LyricsLinesView gains an opt-in centered, desktop default false). - Local Library: the media funnel and an Open (ephemeral) entry point reach the kiosk; Artists is its own multi-column round-card grid (kiosk-only, the desktop LocalArtistsTab is untouched); the Artists drill-down query no longer hangs off a stale derived guard. - Search: the All-tab dashboard shelves render again — they were mounted in a Loader Component that read the delegate id under reuse and bound an empty model; they are direct children now. - Album detail: icon actions the height of the quality badge plus play next / later / queue, an inline floating quality badge, and zebra rows; both album headers show a quality badge. - Immersive: a quiet auto-hiding kiosk exit moved to the bottom, clear of the window controls. Regression coverage in scripts/test_qt_kiosk_feedback.mjs (wired into the runtime gate). Verified on a private authenticated Xvfb; QML audits 6/6, Node gate 5/5, release build 0 warnings. cc
142 lines
7.7 KiB
QML
142 lines
7.7 KiB
QML
// Album header and a recycled track viewport. Playback actions remain album-context actions.
|
|
import QtQuick
|
|
import com.blitzfc.qbz
|
|
import "../theme"
|
|
import "../controls"
|
|
|
|
Rectangle {
|
|
id: root
|
|
color: "transparent"
|
|
QbzTheme { id: theme }
|
|
readonly property var albumDoc: { try { return JSON.parse(QbzAlbum.albumJson) } catch(e) { return ({}) } }
|
|
readonly property var header: albumDoc.header || ({})
|
|
readonly property var tracks: albumDoc.tracks || []
|
|
readonly property bool contentFocus: QbzKioskNav.navActive && QbzKioskNav.zone === "content"
|
|
function publishNav() { QbzKioskNav.publishNav(0, 1, tracks.length, false) }
|
|
function playTrack(id) { QbzPlayer.playAlbumFrom(header.id || "", id) }
|
|
onTracksChanged: Qt.callLater(publishNav)
|
|
Component.onCompleted: publishNav()
|
|
property real negativeRestore: NaN
|
|
function restoreHeader() {
|
|
if (isFinite(negativeRestore) && !QbzAlbum.albumLoading && header.id) {
|
|
list.contentY = negativeRestore
|
|
negativeRestore = NaN
|
|
}
|
|
}
|
|
onHeaderChanged: Qt.callLater(restoreHeader)
|
|
Connections {
|
|
target: QbzAlbum
|
|
function onAlbumLoadingChanged() {
|
|
Qt.callLater(root.restoreHeader)
|
|
}
|
|
}
|
|
KioskNavigation {
|
|
route: "album"
|
|
snapshot: ({ headerY: list.contentY <= 0 ? list.contentY : null })
|
|
onRestore: function(saved) {
|
|
if (typeof saved.headerY === "number" && saved.headerY <= 0) {
|
|
root.negativeRestore = saved.headerY
|
|
Qt.callLater(root.restoreHeader)
|
|
}
|
|
}
|
|
}
|
|
Connections {
|
|
target: QbzKioskNav
|
|
function onIndexChanged() { if (root.contentFocus) list.positionViewAtIndex(QbzKioskNav.index, ListView.Contain) }
|
|
function onActivateSeqChanged() {
|
|
var i = QbzKioskNav.index
|
|
if (root.contentFocus && i >= 0 && i < root.tracks.length) root.playTrack(root.tracks[i].id || "")
|
|
}
|
|
}
|
|
ListView {
|
|
id: list
|
|
anchors.fill: parent
|
|
leftMargin: 16; rightMargin: 16; topMargin: 16; bottomMargin: 16
|
|
clip: true; cacheBuffer: 0; reuseItems: true; spacing: 2
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
model: root.tracks
|
|
header: Item {
|
|
width: list.width - 32
|
|
height: Math.max(180, headerInfo.implicitHeight) + 22
|
|
Rectangle {
|
|
id: coverTile
|
|
width: Math.min(180, list.width * 0.25); height: width
|
|
radius: theme.radiusMd; color: theme.surfaceElevated
|
|
KioskCoverSource { id: cover; remote: root.header.artUrl || ""; local: root.header.artPath || ""; edge: coverTile.width }
|
|
KioskArtwork { anchors.fill: parent; source: cover.source; radius: theme.radiusMd }
|
|
}
|
|
Column {
|
|
id: headerInfo
|
|
anchors.left: coverTile.right; anchors.leftMargin: 16; anchors.right: parent.right
|
|
spacing: 8
|
|
Text {
|
|
width: parent.width; text: root.header.title || ""
|
|
color: theme.textPrimary; font.pixelSize: 25; font.weight: theme.weightBold
|
|
wrapMode: Text.WordWrap; maximumLineCount: 2; elide: Text.ElideRight
|
|
}
|
|
Item {
|
|
width: parent.width; height: 44
|
|
Text { anchors.fill: parent; verticalAlignment: Text.AlignVCenter; text: root.header.artist || ""; color: theme.textSecondary; font.pixelSize: 17; elide: Text.ElideRight }
|
|
MouseArea { anchors.fill: parent; onClicked: QbzArtist.openArtist(root.header.artistId || "") }
|
|
}
|
|
// Action row (owner feedback 2026-09-07): icon buttons, not
|
|
// text; Play/Shuffle plus the three queue insertions
|
|
// (play next / play later / add to queue), all the SAME height;
|
|
// and the quality badge floats inline at the right of the same
|
|
// row instead of owning a line above it.
|
|
Item {
|
|
width: parent.width
|
|
height: 48
|
|
Row {
|
|
id: albumActions
|
|
anchors.left: parent.left
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 8
|
|
QbzIconButton { name: "play-fill"; btnSize: 48; iconSize: 22; activeBackground: true; active: true; onClicked: QbzPlayer.playAlbum(root.header.id || "") }
|
|
QbzIconButton { name: "shuffle"; btnSize: 48; iconSize: 20; active: QbzPlayer.npShuffle; onClicked: QbzPlayer.playAlbumShuffled(root.header.id || "") }
|
|
QbzIconButton { name: "list-start"; btnSize: 48; iconSize: 20; onClicked: QbzPlayer.enqueueAlbum(root.header.id || "", "next") }
|
|
QbzIconButton { name: "list-plus"; btnSize: 48; iconSize: 20; onClicked: QbzPlayer.enqueueAlbum(root.header.id || "", "later") }
|
|
QbzIconButton { name: "list-end"; btnSize: 48; iconSize: 20; onClicked: QbzPlayer.enqueueAlbum(root.header.id || "", "queue") }
|
|
}
|
|
QualityBadgeFull {
|
|
readonly property string rawTier: root.header.qualityTier || ""
|
|
visible: tier !== "" || detail !== ""
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
tier: rawTier === "max" ? "hires" : rawTier === "lossy" ? "mp3" : rawTier
|
|
detail: root.header.qualityDetail || ""
|
|
showIcon: true
|
|
scaleFactor: 1.15
|
|
}
|
|
}
|
|
}
|
|
}
|
|
delegate: Rectangle {
|
|
id: row
|
|
required property var modelData
|
|
required property int index
|
|
width: list.width - 32; height: 64; radius: theme.radiusSm
|
|
readonly property bool current: QbzPlayer.npTrackId === (modelData.id || "")
|
|
readonly property bool navFocused: root.contentFocus && QbzKioskNav.index === index
|
|
// Zebra striping (owner feedback 2026-09-07): odd rows carry a faint
|
|
// fill so a long tracklist reads as banded rows. Current/nav/focus
|
|
// states win over the stripe.
|
|
color: navFocused ? Qt.rgba(theme.accent.r, theme.accent.g, theme.accent.b, 0.18)
|
|
: current ? theme.surfaceElevated
|
|
: (index % 2 === 1 ? theme.surfaceHover : "transparent")
|
|
border.width: navFocused ? 2 : 0; border.color: theme.accent
|
|
Text { id: number; x: 8; width: 34; height: parent.height; text: modelData.number || (index + 1); color: theme.textMuted; font.pixelSize: 14; verticalAlignment: Text.AlignVCenter; horizontalAlignment: Text.AlignRight }
|
|
Text { id: duration; anchors.right: parent.right; anchors.rightMargin: 12; height: parent.height; text: modelData.duration || ""; color: theme.textMuted; font.pixelSize: 13; verticalAlignment: Text.AlignVCenter }
|
|
Text { anchors.left: number.right; anchors.leftMargin: 14; anchors.right: duration.left; anchors.rightMargin: 14; height: parent.height; text: modelData.title || ""; color: row.current ? theme.accent : theme.textPrimary; font.pixelSize: 16; elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter }
|
|
MouseArea { anchors.fill: parent; onClicked: root.playTrack(modelData.id || "") }
|
|
}
|
|
}
|
|
ScrollMemory { target: list; scope: "album" }
|
|
KioskSkeleton {
|
|
anchors.fill: parent; kind: "detail"
|
|
loading: QbzAlbum.albumLoading && root.tracks.length === 0
|
|
error: root.albumDoc.error || ""
|
|
empty: !QbzAlbum.albumLoading && !root.header.id && root.tracks.length === 0
|
|
}
|
|
}
|