Files
qbz/crates/qbz-qt/qml/kiosk/KioskTrackRow.qml
T
vicrodh b1dba198c9 feat(qt): kiosk profile release hardening
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
2026-09-07 08:23:11 -06:00

163 lines
6.1 KiB
QML

// KioskTrackRow — lightweight track row for the kiosk Search/Library track
// tabs (shell/KioskTrackRow.slint). Art + title/artist + duration, tap to
// play. No number cell, no favourite, no quality badge, no menu, no
// checkbox — the desktop rows/TrackRow.qml column model is exactly the weight
// the kiosk shell drops.
//
// `track` is a plain JS object carrying { id, title, artist, duration,
// artwork }; `artwork` is the RESOLVED local cover path the hosting view
// supplies (the Qt documents carry only the remote `artUrl`).
//
// `playing` marks the row whose track is the one on the deck. The Slint
// KioskTrackRow has no such state, so its treatment is taken from the kiosk's
// own current-track idiom on the album page (KioskAlbum.slint:171-175, :195):
// an accent@0.10 fill that sits BELOW focus and hover in the precedence, and
// the title in the accent colour.
//
// ── TOUCH HEIGHT ──────────────────────────────────────────────────────────
// 64 px, not the 62 this file shipped. Contract §2.6 puts the PRIMARY touch
// target at "al menos 64 px lógicos cuando hay espacio", and a track row is
// the primary target of every list surface in the kiosk — Library/Search
// Tracks, the album track list, the queue. Two pixels is not a visual change;
// it is the difference between meeting the floor and missing it by a hair on
// every list in the shell.
//
// The whole row is ONE hit area, so the target IS the height: there is no
// number cell, favourite, badge, menu or checkbox to subdivide it (that
// absence is the point of this component). `rowHeight` stays a property so a
// denser surface can opt out with evidence rather than by editing this file.
//
// The art tile stays 46 and centres in the taller row. It is a thumbnail, not
// a target, and growing it would enlarge every artwork request in the shell
// for no touch benefit.
//
// ARTWORK: KioskArtwork, not theme/RoundedImage — see KioskCard's header. In
// a 46 px tile the difference is the largest in the shell: the desktop path
// would decode a 600 px original to discover it should have asked for ~50.
import QtQuick
import "../theme"
Rectangle {
id: root
property var track: ({})
/// Keyboard/gamepad nav — same ring idiom as KioskCard, 2px here.
property bool navFocused: false
property bool playing: false
/// The primary touch target of every kiosk list (contract §2.6: ≥64 px).
property real rowHeight: 64
/// Thumbnail edge. Not a touch target; it drives the artwork request.
property real artSize: 46
signal clicked()
/// The thumbnail is DECODED and on screen — the KioskCard.artReady
/// contract. A row with no artwork slot never becomes ready, which is
/// correct: `_artwork` is "" and there is nothing to wait for.
readonly property bool artReady: art.ready
QbzTheme { id: theme }
readonly property string _title: root.track && root.track.title ? root.track.title : ""
readonly property string _artist: root.track && root.track.artist ? root.track.artist : ""
readonly property string _duration: root.track && root.track.duration ? root.track.duration : ""
readonly property string _artwork: root.track && root.track.artwork ? root.track.artwork : ""
height: root.rowHeight
radius: theme.radiusSm
color: root.navFocused
? Qt.rgba(theme.accent.r, theme.accent.g, theme.accent.b, 0.18)
: rowArea.containsMouse
? theme.alphaTier(6)
: root.playing
? Qt.rgba(theme.accent.r, theme.accent.g, theme.accent.b, 0.10)
: "transparent"
border.width: root.navFocused ? 2 : 0
border.color: theme.accent
Row {
id: cells
anchors.left: root.left
anchors.leftMargin: 8
anchors.right: root.right
anchors.rightMargin: 14
anchors.top: root.top
anchors.bottom: root.bottom
spacing: 12
// Art slot — a fixed square thumbnail centred in the row height.
Item {
id: artCell
width: root.artSize
height: cells.height
Rectangle {
id: artTile
width: root.artSize
height: root.artSize
y: (artCell.height - artTile.height) / 2
radius: 5
color: theme.surfaceElevated
clip: true
KioskArtwork {
id: art
anchors.fill: parent
source: root._artwork
radius: 5
fit: "crop"
}
}
}
Item {
id: textCol
width: Math.max(0, cells.width - artCell.width - durationLabel.width - 2 * cells.spacing)
height: cells.height
Column {
anchors.verticalCenter: textCol.verticalCenter
width: textCol.width
spacing: 2
Text {
width: textCol.width
text: root._title
color: root.playing ? theme.accent : theme.textPrimary
font.pixelSize: 15
font.weight: theme.weightMedium
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
}
Text {
width: textCol.width
text: root._artist
color: theme.textMuted
font.pixelSize: 13
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
}
}
}
Text {
id: durationLabel
height: cells.height
text: root._duration
color: theme.textMuted
font.pixelSize: 13
verticalAlignment: Text.AlignVCenter
}
}
MouseArea {
id: rowArea
anchors.fill: parent
hoverEnabled: true
onClicked: root.clicked()
}
}