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
This commit is contained in:
vicrodh committed 2026-09-07 08:23:11 -06:00
1 parent c1c463dc94
commit b1dba198c9
139 files changed
+9590 -4973

No files matched your search

@@ -50,6 +50,7 @@ import "playlistmanager"
Rectangle {
id: root
property bool kioskHost: false
// Round to the AppShell content-frame bezel: QML clips are rectangular, so
// the frame's rounding never reaches the view — the view's own fill must
@@ -104,8 +105,8 @@ Rectangle {
// ---- geometry --------------------------------------------------------
readonly property int contentW: Math.max(0, pmList.width - 64)
readonly property int gridCols: Math.max(1, Math.floor((root.contentW + 16) / 176))
readonly property int cardH: root.canReorder ? 250 : 220
readonly property int gridCols: Math.max(1, Math.floor((root.contentW + 16) / (root.kioskHost ? 216 : 176)))
readonly property int cardH: root.kioskHost ? (root.canReorder ? 320 : 270) : (root.canReorder ? 250 : 220)
readonly property int rowsInBody:
root.viewMode === "tree" ? root.tree.length : root.playlists.length
readonly property int rowCount:
@@ -130,7 +131,7 @@ Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: 56
height: root.kioskHost ? 64 : 56
// The reference paints this bar an opaque `surface-main` while the view
// root goes transparent (`:1089`, `:1097`) — under the dynamic
@@ -153,6 +154,7 @@ Rectangle {
// views/PlaylistBrowseView.qml:143 chose for the byte-identical Slint
// header, so the port's two 56 px headers align.
Text {
visible: !root.kioskHost
x: 48
y: 25 - height / 2
width: Math.max(0, tools.x - 48 - 16)
@@ -163,8 +165,19 @@ Rectangle {
elide: Text.ElideRight
}
Row {
visible: root.kioskHost
x: 16
height: 64
spacing: 12
Text { width: 190; height: 64; text: QbzSession.tr("Playlist manager",QbzSession.trRev); color: theme.textPrimary; font.pixelSize: 20; elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter }
QbzLineEdit { kioskHost: true; width: Math.max(120,root.width-306); anchors.verticalCenter: parent.verticalCenter; searchMode: true; text: root.searchSeed; placeholder: QbzSession.tr("Search playlists…",QbzSession.trRev); onEdited: function(v) { QbzPlaylistManager.searchChanged(v) } }
SettingsButton { id: kioskActions; kioskHost: true; minWidth: 64; btnHeight: 64; text: "⋯"; onClicked: kioskMenu.openBelowRight(kioskActions) }
}
PmToolbar {
id: tools
visible: !root.kioskHost
x: Math.max(0, parent.width - width - 32)
y: 25 - height / 2
searchSeed: root.searchSeed
@@ -176,6 +189,36 @@ Rectangle {
}
}
CardMenu {
id: kioskMenu
kioskHost: root.kioskHost
menuWidth: Math.min(root.width-24,320)
entries: [
{label: QbzSession.tr("Import",QbzSession.trRev),action:"import"},
{label: QbzSession.tr("New folder",QbzSession.trRev),action:"new-folder"},
{label: QbzSession.tr("Folders",QbzSession.trRev),action:"folders"},
{label: QbzSession.tr("Grid",QbzSession.trRev),action:"view:grid"},
{label: QbzSession.tr("List",QbzSession.trRev),action:"view:list"},
{label: QbzSession.tr("Tree",QbzSession.trRev),action:"view:tree"},
{label: QbzSession.tr("All",QbzSession.trRev),action:"filter:all"},
{label: QbzSession.tr("Visible",QbzSession.trRev),action:"filter:visible"},
{label: QbzSession.tr("Hidden",QbzSession.trRev),action:"filter:hidden"},
{label: QbzSession.tr("Name",QbzSession.trRev),action:"sort:name"},
{label: QbzSession.tr("Tracks",QbzSession.trRev),action:"sort:tracks"},
{label: QbzSession.tr("Recent",QbzSession.trRev),action:"sort:recent"},
{label: QbzSession.tr("Play Count",QbzSession.trRev),action:"sort:playcount"},
{label: QbzSession.tr("Custom",QbzSession.trRev),action:"sort:custom"}
]
onPicked: function(a) {
if(a==="import") QbzPlaylistImport.open()
else if(a==="new-folder") QbzFolderEdit.openNewFolder()
else if(a==="folders") QbzPlaylistManager.toggleFolderMode()
else if(a.indexOf("view:")===0) QbzPlaylistManager.setViewMode(a.slice(5))
else if(a.indexOf("filter:")===0) QbzPlaylistManager.setFilter(a.slice(7))
else if(a.indexOf("sort:")===0) QbzPlaylistManager.setSort(a.slice(5))
}
}
// ================================ body ================================
Item {
id: bodyHost
@@ -191,7 +234,7 @@ Rectangle {
boundsBehavior: Flickable.StopAtBounds
spacing: 0
// ~2 grid rows of pre-instantiation past the viewport.
cacheBuffer: 400
cacheBuffer: root.kioskHost ? 128 : 400
reuseItems: true
// An INT model in all three modes: the delegates read
@@ -201,6 +244,8 @@ Rectangle {
model: root.rowCount
header: PmPageHead {
kioskHost: root.kioskHost
viewport: pmList
width: pmList.width
loading: QbzPlaylistManager.pmLoading
folders: root.folders
@@ -285,6 +330,7 @@ Rectangle {
model: cell.slice
delegate: PmGridCard {
kioskHost: root.kioskHost
required property var modelData
required property int index
@@ -311,9 +357,10 @@ Rectangle {
readonly property var entry: root.playlists[lrow.index] || ({})
width: pmList.width
height: 68
height: root.kioskHost && root.canReorder ? 92 : 68
PmListRow {
kioskHost: root.kioskHost
x: 32
width: root.contentW
item: lrow.entry
@@ -345,12 +392,12 @@ Rectangle {
readonly property bool isFolder: trow.entry.kind === "folder"
width: pmList.width
height: (trow.isFolder ? 44 : 40) + 2
height: (root.kioskHost ? 64 : (trow.isFolder ? 44 : 40)) + 2
Loader {
x: 32
width: root.contentW
height: trow.isFolder ? 44 : 40
height: root.kioskHost ? 64 : (trow.isFolder ? 44 : 40)
sourceComponent: trow.isFolder ? treeFolderArm : treePlaylistArm
}
@@ -358,6 +405,7 @@ Rectangle {
id: treeFolderArm
PmTreeFolderRow {
kioskHost: root.kioskHost
folder: trow.entry.folder || ({})
expanded: trow.entry.expanded === true
}
@@ -366,6 +414,7 @@ Rectangle {
id: treePlaylistArm
PmTreePlaylistRow {
kioskHost: root.kioskHost
item: trow.entry.playlist || ({})
indent: trow.entry.indent === true
}
@@ -379,6 +428,7 @@ Rectangle {
// own Popup.
PmFolderMenu {
id: folderMenu
kioskHost: root.kioskHost
folders: root.folders
}
}