Files
qbz/crates/qbz-qt/qml/views/playlistmanager/PmTreePlaylistRow.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

145 lines
4.9 KiB
QML

// PmTreePlaylistRow — a playlist row of the TREE mode
// (PlaylistManagerView.slint:947-1013). 40 px, r6, transparent →
// `surface-hover` on body hover, 0.6 opacity while hidden. A row inside a
// folder indents to 36 px, a root row starts at 10 (`:957`).
//
// Affordances (contract D27): open (body), favourite and edit-playlist ONLY —
// no visibility toggle, no mixtape, no move-to-folder, no reorder chevrons. The
// two buttons are HOVER-REVEALED, which the grid card's and the list row's are
// not.
//
// TWO deliberate deviations, both recorded in the contract:
//
// * D26 — the reference tree carries no `hard-drive` marker for a local
// playlist although the list row does. Added, with the list row's tint rule
// (`accent` when offline-only, else `text-muted`).
// * D27 — the reference binds the buttons' opacity to the ROW TouchArea,
// which is `parent.width - 90px` and therefore EXCLUDES the buttons: moving
// the pointer onto a button drops it to opacity 0 while it still accepts
// the click. Here a `HoverHandler` on the row keeps them lit under the
// pointer. The row FILL still follows the body area, exactly like the
// reference.
//
// INTERFACE NOTE (contract §2.5): the contract declares `item` only. `indent`
// is ADDED because the indentation is a property of the TREE ROW
// (`tree[i].indent`, §4.2), not of the playlist — without it every nested row
// would sit at the root gutter and the tree would read flat.
import QtQuick
import "../../kiosk"
import com.blitzfc.qbz
import "../../cards"
import "../../theme"
Rectangle {
id: root
property bool kioskHost: false
/// `managerJson.tree[i].playlist` — one `PlaylistRow` (contract §4.3).
property var item: ({})
/// `tree[i].indent` — true for a folder member.
property bool indent: false
QbzTheme { id: theme }
readonly property string itemId: String(root.item.id || "")
readonly property bool isLocal: root.item.isLocal === true
height: root.kioskHost ? 64 : 40
radius: 6
color: bodyArea.containsMouse ? theme.surfaceHover : "transparent"
opacity: (root.item.isHidden === true) ? 0.6 : 1.0
HoverHandler { id: rowHover }
Loader {
id: collage
x: root.indent ? 36 : 10
anchors.verticalCenter: parent.verticalCenter
width: 28; height: 28
sourceComponent: root.kioskHost ? kioskCover : desktopCover
}
Component { id: kioskCover; KioskArtwork { source: (root.item.covers || [])[0] || ""; radius: 5 } }
Component { id: desktopCover;
PlaylistCollage {
id: desktopCollage
x: 0
anchors.verticalCenter: parent.verticalCenter
width: 28
height: 28
layout: "pm"
urls: root.item.covers || []
radius: 5
}
}
Row {
id: nameRow
x: collage.x + 28 + 10
anchors.verticalCenter: parent.verticalCenter
width: Math.max(0, tracksText.x - 10 - x)
spacing: 6
QbzIcon {
visible: root.isLocal
anchors.verticalCenter: parent.verticalCenter
name: "hard-drive"
width: 13
height: 13
tintName: root.item.offlineOnly === true ? "accent" : "muted"
}
Text {
anchors.verticalCenter: parent.verticalCenter
width: Math.max(0, parent.width - (root.isLocal ? 19 : 0))
text: root.item.name || ""
color: theme.textPrimary
font.pixelSize: theme.fontBody
elide: Text.ElideRight
}
}
Text {
id: tracksText
x: actions.x - 10 - width
anchors.verticalCenter: parent.verticalCenter
text: root.item.tracksLine || ""
color: theme.textMuted
font.pixelSize: theme.fontLegal
}
Row {
id: actions
x: root.width - 12 - width
anchors.verticalCenter: parent.verticalCenter
spacing: 0
opacity: root.kioskHost || rowHover.hovered ? 1.0 : 0.0
PmActionButton {
width: root.kioskHost ? 44 : 26
height: root.kioskHost ? 44 : 26
name: root.item.isFavorite === true ? "heart-filled" : "heart"
filled: root.item.isFavorite === true
onClicked: QbzPlaylistManager.toggleFavorite(root.itemId)
}
PmActionButton {
width: root.kioskHost ? 44 : 26
height: root.kioskHost ? 44 : 26
name: "pen-line"
onClicked: QbzPlaylistEdit.open(root.itemId)
}
}
// Body: everything left of the two buttons' 90 px reserve (`:1006`).
MouseArea {
id: bodyArea
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: Math.max(0, parent.width - 90)
z: -1
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: QbzBridge.openPlaylist(root.itemId)
}
}