Contract sections 4, 5, 6 and the QML half of 7.
ContentRouter.qml is the D3 extraction: ONE view-mount chain, shared by both
shells. The Slint kiosk's content block is a verbatim copy of AppShell's and
KioskShell.slint:10-17 names this extraction as the follow-up it was waiting
for. 23 desktop arms carried over with their comments, 8 kiosk overrides, and
the kiosk-only nowplaying route. AppShell's four viewLoader.item sites now go
through contentRouter.currentItem; a tree-wide grep confirmed no other
reference (LESSONS L4 -- grep, do not trust memory about "both places").
KioskShell.qml carries the back bar with the full search focus lifecycle, the
content-height arithmetic including the now-playing exception, the offline
gate, the forced small transport, the player-zone focus ring, the root key
handler and the 30ms initial focus grab. Two findings worth keeping:
- the NavRail tiles fire header-menu-navigate("favorites-tracks") in the
reference, which switches the view AND selects a landing tab; navigateTo
carries only the route, so the tiles reuse the port's existing pending-tab
handshake rather than landing on the wrong tab.
- the visualizer tile's action is a sanctioned no-op (R2). Wiring
QbzImmersive would be worse than nothing: `open` would go true over an
empty mount and the key guard would then reject every key against a
surface that renders nothing.
Three fractional font sizes had to be rounded. Qt's font.pixelSize is an INT
property, so the reference's 11.5px (NavRail.slint:47) and 12.5px
(KioskNowPlaying.slint:59,67) are not expressible; font.pointSize is the only
real-valued alternative and is DPI-relative, which would make those three
labels scale differently from every other in the tree.
The NavRail one was caught by the offscreen smoke run IN THE KIOSK PROFILE --
it made the whole rail unavailable, i.e. a kiosk with no footer. The build,
both QML audits and qmllint were all green while that was true, and the
desktop smoke was clean because it never mounts KioskShell. The other two
were still latent: KioskNowPlaying is not on the smoke's route.
Gate: build 9 warnings = baseline, 291 tests, both audits OK over 229 QML
files (+15), qmllint filtered = the same 20 pre-existing ArtistView lines and
nothing else, kiosk-profile offscreen smoke clean.
cc
129 lines
4.7 KiB
QML
129 lines
4.7 KiB
QML
// KioskCard — the lightweight album card for the kiosk browse shelves
|
|
// (shell/KioskCard.slint). Deliberately SIMPLER than cards/AlbumCard.qml:
|
|
// square art + a quality badge + title + artist, tap-to-open. No hover-reveal
|
|
// actions, no context menu, no source/ribbon/award chrome, no pin badge, no
|
|
// favourite heart, no selection checkbox, no hover scale or scrim — that
|
|
// weight is what made the reused desktop views too heavy for a Pi. The only
|
|
// interaction is a full-surface tap.
|
|
//
|
|
// `album` is a plain JS object carrying { id, title, artist, artwork,
|
|
// qualityTier }. `artwork` is the RESOLVED local cover path: the Slint reads
|
|
// AlbumCardItem.artwork, which the artwork pipeline pushes into the shared
|
|
// model in place; the Qt documents carry only the remote `artUrl`, so the
|
|
// HOSTING VIEW resolves it and hands the local path down under `artwork`.
|
|
//
|
|
// Height is INTRINSIC (art + 7 + title line + 7 + artist line). The grid slot
|
|
// allocates artSize + 46 and the column's top alignment leaves the slack at
|
|
// the bottom, exactly as the Slint VerticalLayout's `alignment: start` does.
|
|
|
|
import QtQuick
|
|
import "../theme"
|
|
|
|
Rectangle {
|
|
id: root
|
|
|
|
property var album: ({})
|
|
property real artSize: 128
|
|
/// Circular art + centred text. No kiosk view sets it today; it is a
|
|
/// declared parameter of the primitive in the Slint (KioskCard.slint:15)
|
|
/// and drives the art radius and both text alignments.
|
|
property bool round: false
|
|
/// Keyboard/gamepad nav: the hosting view sets this when the shell focus
|
|
/// index lands on this card. A 3px accent ring + tint, readable at
|
|
/// 10-foot distance.
|
|
property bool navFocused: false
|
|
signal clicked(string id)
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
readonly property string _title: root.album && root.album.title ? root.album.title : ""
|
|
readonly property string _artist: root.album && root.album.artist ? root.album.artist : ""
|
|
readonly property string _artwork: root.album && root.album.artwork ? root.album.artwork : ""
|
|
readonly property string _tier: root.album && root.album.qualityTier ? root.album.qualityTier : ""
|
|
|
|
implicitWidth: root.artSize
|
|
width: root.artSize
|
|
implicitHeight: body.implicitHeight
|
|
border.width: root.navFocused ? 3 : 0
|
|
border.color: theme.accent
|
|
radius: theme.radiusSm
|
|
color: root.navFocused
|
|
? Qt.rgba(theme.accent.r, theme.accent.g, theme.accent.b, 0.12)
|
|
: "transparent"
|
|
|
|
Column {
|
|
id: body
|
|
width: root.artSize
|
|
spacing: 7
|
|
|
|
Rectangle {
|
|
id: artTile
|
|
width: root.artSize
|
|
height: root.artSize
|
|
radius: root.round ? root.artSize / 2 : theme.radiusSm
|
|
color: theme.surfaceElevated
|
|
clip: true
|
|
|
|
RoundedImage {
|
|
anchors.fill: parent
|
|
source: root._artwork
|
|
radius: artTile.radius
|
|
fit: "crop"
|
|
}
|
|
|
|
// Quality badge — bottom-left, 6px inset on both axes, only for
|
|
// hi-res / lossless.
|
|
Rectangle {
|
|
id: badge
|
|
visible: root._tier !== ""
|
|
x: 6
|
|
y: artTile.height - badge.height - 6
|
|
height: 19
|
|
width: badgeLabel.implicitWidth + 12
|
|
color: "#04070cd9"
|
|
radius: 4
|
|
|
|
Text {
|
|
id: badgeLabel
|
|
anchors.left: badge.left
|
|
anchors.leftMargin: 6
|
|
anchors.verticalCenter: badge.verticalCenter
|
|
text: root._tier === "hires" ? "Hi-Res" : "CD"
|
|
color: root._tier === "hires" ? "#e6b24d" : theme.textSecondary
|
|
font.pixelSize: 10
|
|
font.weight: theme.weightBold
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
width: root.artSize
|
|
text: root._title
|
|
color: theme.textPrimary
|
|
font.pixelSize: 14
|
|
font.weight: theme.weightSemibold
|
|
elide: Text.ElideRight
|
|
wrapMode: Text.NoWrap
|
|
maximumLineCount: 1
|
|
horizontalAlignment: root.round ? Text.AlignHCenter : Text.AlignLeft
|
|
}
|
|
|
|
Text {
|
|
width: root.artSize
|
|
text: root._artist
|
|
color: theme.textMuted
|
|
font.pixelSize: 13
|
|
elide: Text.ElideRight
|
|
wrapMode: Text.NoWrap
|
|
maximumLineCount: 1
|
|
horizontalAlignment: root.round ? Text.AlignHCenter : Text.AlignLeft
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
hoverEnabled: false
|
|
onClicked: root.clicked(root.album && root.album.id ? root.album.id : "")
|
|
}
|
|
}
|