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
214 lines
8.5 KiB
QML
214 lines
8.5 KiB
QML
// PlaylistListRow — 1:1 port of primitives/PlaylistListRow.slint: the LIST
|
|
// arm of the Qobuz Playlists browse page. 60px, radius 8, odd-row zebra,
|
|
// 44px thumb, name over "owner • N tracks", a play affordance and the ⋯
|
|
// menu. The whole row body opens the playlist.
|
|
//
|
|
// item contract: home_qt::HomeCard as the playlist pages publish it —
|
|
// { id, title, subtitle, artPath }.
|
|
//
|
|
// Menu inventory vs the .slint's five rows: Play / Play next / Play later /
|
|
// Add to queue are wired through QbzPlayer; "Follow on Qobuz" is NOT — the
|
|
// follow invokable takes the playlist that is currently OPEN
|
|
// (`playlistToggleFollow`), so a row cannot call it, exactly as
|
|
// PlaylistCard.qml documents for "Copy to your library". The entry stays out
|
|
// of the menu rather than rendering and doing nothing.
|
|
|
|
import QtQuick
|
|
import com.blitzfc.qbz
|
|
import "../cards"
|
|
import "../controls"
|
|
import "../theme"
|
|
import "../kiosk"
|
|
|
|
Rectangle {
|
|
id: root
|
|
property bool kioskHost: false
|
|
property bool pinned: item.isPinned === true
|
|
Connections { target: QbzLibrary; function onPinChanged(key, value) { if (key === "playlist:" + (root.item.id || "")) root.pinned = value } }
|
|
|
|
property var item: ({})
|
|
/// Cover override for hosts whose rows carry no `artPath` of their own.
|
|
/// The Library feed is id-keyed (`artKey` -> a decoded file:// path in
|
|
/// LibraryView's artMap); "" falls straight back to `item.artPath`, so the
|
|
/// browse pages are untouched.
|
|
property string artSource: ""
|
|
/// Up to four member-track covers — the 2x2 mosaic a USER playlist shows
|
|
/// when it has no artwork of its own (`library_qt::FeedItem.covers`). The
|
|
/// browse pages publish none, so they keep the designed glyph placeholder.
|
|
property var covers: []
|
|
readonly property string artResolved:
|
|
root.artSource !== "" ? root.artSource : (root.item.artPath || "")
|
|
/// Row index — drives the even/odd zebra (coherent with TrackRow).
|
|
/// NOT named `index`: a Repeater injects one into the delegate context
|
|
/// and a same-named property here would shadow it.
|
|
property int rowIndex: 0
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
readonly property bool rowHovered: rowArea.containsMouse || playArea.containsMouse
|
|
|| moreArea.containsMouse
|
|
|
|
width: parent ? parent.width : 0
|
|
height: root.kioskHost ? 64 : 60
|
|
radius: theme.radiusSm
|
|
color: rowHovered ? theme.surfaceHover
|
|
: (rowIndex % 2 === 1 ? theme.alphaTier(4) : "transparent")
|
|
|
|
MouseArea {
|
|
id: rowArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: function (mouse) {
|
|
if (mouse.button === Qt.RightButton)
|
|
root.openMenu(rowArea, mouse.x, mouse.y)
|
|
else
|
|
QbzBridge.openPlaylist(root.item.id || "")
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
id: rowMenuLoader
|
|
active: false
|
|
sourceComponent: CardMenu {
|
|
kioskHost: root.kioskHost
|
|
menuWidth: root.kioskHost ? 252 : 196
|
|
entries: [
|
|
{ "label": QbzSession.tr("Play", QbzSession.trRev), "icon": "play-fill", "action": "play" },
|
|
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "action": "next" },
|
|
{ "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "action": "later" },
|
|
{ "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "action": "queue" },
|
|
].concat(root.kioskHost ? [
|
|
{label: QbzSession.tr(root.pinned ? "Unpin" : "Pin", QbzSession.trRev), icon: "pin", action: "pin"},
|
|
{label: QbzSession.tr("Add to mixtape", QbzSession.trRev), icon: "cassette-tape", action: "mixtape"},
|
|
{label: QbzSession.tr("Make available offline", QbzSession.trRev), icon: "cloud-download", action: "cache"}
|
|
] : [])
|
|
onPicked: function (a) {
|
|
var id = root.item.id || ""
|
|
if (id === "") return
|
|
if (a === "play") QbzPlayer.playPlaylistById(id)
|
|
else if (a === "pin") QbzLibrary.togglePin("playlist", id, root.item.title || "", root.item.subtitle || "", root.item.artUrl || "")
|
|
else if (a === "cache") QbzOffline.cachePlaylist(String(id))
|
|
else if (a === "mixtape") QbzMyQbzAdd.open(JSON.stringify([{itemType: "playlist", source: "qobuz", sourceItemId: String(id), title: root.item.title || "", subtitle: root.item.subtitle || "", artworkUrl: root.item.artUrl || ""}]))
|
|
else QbzPlayer.enqueuePlaylistById(id, a)
|
|
}
|
|
}
|
|
}
|
|
function openMenu(anchor, x, y) {
|
|
rowMenuLoader.active = true
|
|
rowMenuLoader.item.openAtCursor(anchor, x, y)
|
|
}
|
|
function releaseForReuse() {
|
|
if (rowMenuLoader.item)
|
|
rowMenuLoader.item.close()
|
|
rowMenuLoader.active = false
|
|
}
|
|
ListView.onPooled: root.releaseForReuse()
|
|
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 10
|
|
anchors.rightMargin: 10
|
|
spacing: 12
|
|
|
|
Rectangle {
|
|
width: 44
|
|
height: 44
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
radius: 6
|
|
color: theme.surfaceElevated
|
|
clip: true
|
|
// These rows come from `home_qt::map_playlist` (browse_qt.rs:612),
|
|
// whose art is the playlist's own `image.rectangle` — 800x380.
|
|
// `auto` pads that instead of cropping to the middle 47% of it,
|
|
// and still crops the square `image.covers[0]` fallback.
|
|
KioskArtwork {
|
|
anchors.fill: parent
|
|
visible: root.kioskHost
|
|
source: root.kioskHost ? root.artResolved : ""
|
|
}
|
|
RoundedImage {
|
|
anchors.fill: parent
|
|
visible: !root.kioskHost && !rowCollage.visible
|
|
source: root.kioskHost ? "" : root.artResolved
|
|
radius: 6
|
|
fit: "auto"
|
|
}
|
|
PlaylistCollage {
|
|
id: rowCollage
|
|
anchors.fill: parent
|
|
visible: root.artResolved === "" && (root.covers || []).length > 0
|
|
urls: root.covers || []
|
|
radius: 6
|
|
}
|
|
QbzIcon {
|
|
visible: root.artResolved === "" && !rowCollage.visible
|
|
name: "list-music"
|
|
width: 20
|
|
height: 20
|
|
anchors.centerIn: parent
|
|
tintName: "muted"
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width - 44 - 2 * (root.kioskHost ? 44 : 30) - 4 * 12
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 2
|
|
Text {
|
|
width: parent.width
|
|
text: root.item.title || ""
|
|
color: theme.textPrimary
|
|
font.pixelSize: theme.fontBody
|
|
font.weight: theme.weightMedium
|
|
elide: Text.ElideRight
|
|
}
|
|
Text {
|
|
width: parent.width
|
|
text: root.item.subtitle || ""
|
|
color: theme.textMuted
|
|
font.pixelSize: theme.fontLegal
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
|
|
Item {
|
|
width: root.kioskHost ? 44 : 30
|
|
height: parent.height
|
|
QbzIcon {
|
|
name: "play-fill"
|
|
width: 16
|
|
height: 16
|
|
anchors.centerIn: parent
|
|
tintName: root.rowHovered ? "textPrimary" : "muted"
|
|
}
|
|
MouseArea {
|
|
id: playArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: QbzPlayer.playPlaylistById(root.item.id || "")
|
|
}
|
|
}
|
|
Item {
|
|
width: root.kioskHost ? 44 : 30
|
|
height: parent.height
|
|
QbzIcon {
|
|
name: "ellipsis"
|
|
width: 16
|
|
height: 16
|
|
anchors.centerIn: parent
|
|
tintName: moreArea.containsMouse ? "textPrimary" : "secondary"
|
|
}
|
|
MouseArea {
|
|
id: moreArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: function (mouse) { rowMenu.openAtCursor(moreArea, mouse.x, mouse.y) }
|
|
}
|
|
}
|
|
}
|
|
}
|