Files
qbz/crates/qbz-qt/qml/shell/ViewModeMenu.qml
T
vicrodh a92dafa734 fix(player): finalize lyrics and park waveform
Hide the waveform controls and force every now-playing bar back to the classic seekbar while preserving the implementation for a later release. Keep full-screen timed lyrics centered, slightly larger and wrapped without changing the approved typography or fill.

cx
2026-08-25 10:17:43 -06:00

240 lines
9.7 KiB
QML

// Now-Playing-view mode menu (PlayerBar.slint layout-menu, phase 18) —
// one QbzContextMenu shared by the full PlayerBar and the Small bar:
// New / Classic / Small / Large (with the current mode checked), then the
// window-mode rows. All three window-mode rows are LIVE: Immersive since the
// 2026-08-02 immersive-port §7 B2, Kiosk since the 2026-08-02 kiosk-port §8.1,
// Miniplayer since the 2026-08-03 miniplayer/tray-port §4.10 B2.
import QtQuick
import QtQuick.Controls
import com.blitzfc.qbz
import "../controls"
import "../theme"
QbzContextMenu {
id: root
menuWidth: 196
// The theme tokens. Declared HERE even though the BASE type
// (controls/QbzContextMenu.qml) already has a `QbzTheme { id: theme }`:
// a QML id belongs to the document that declares it, and a derived
// document is a DIFFERENT scope — the base's context is created as a
// child of this one, never as its parent, so `theme` was resolving to
// nothing here and every binding below threw a silent ReferenceError
// (leaving the row Rectangles at their default WHITE fill). The four
// sibling menus built on the same base — CardMenu, AudioSettingsMenu,
// PmFolderMenu, SidebarRowMenu — all declare their own; this file was
// the lone outlier.
QbzTheme { id: theme }
Repeater {
model: [
{ "label": QbzSession.tr("New", QbzSession.trRev), "icon": "panel-left", "mode": 0 },
{ "label": QbzSession.tr("Classic", QbzSession.trRev), "icon": "panel-right-close", "mode": 1 },
{ "label": QbzSession.tr("Small", QbzSession.trRev), "icon": "rows-3", "mode": 2 },
{ "label": QbzSession.tr("Large", QbzSession.trRev), "icon": "layout-grid", "mode": 3 },
]
delegate: Rectangle {
required property var modelData
width: parent ? parent.width : 0
height: 33
radius: 5
color: miArea.containsMouse ? theme.surfaceHover : "transparent"
Row {
anchors.fill: parent
anchors.leftMargin: 8
spacing: 8
QbzIcon {
name: modelData.icon
width: 15
height: 15
anchors.verticalCenter: parent.verticalCenter
tintName: "secondary"
}
Text {
height: parent.height
width: parent.width - 23 - 22
text: modelData.label
color: theme.textSecondary
font.pixelSize: 13
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
QbzIcon {
visible: QbzShell.npbMode === modelData.mode
name: "check"
width: 13
height: 13
anchors.verticalCenter: parent.verticalCenter
tintName: "accent"
}
}
MouseArea {
id: miArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.close()
QbzShell.npbSetMode(modelData.mode)
}
}
}
}
Item {
// Park this divider with the hidden waveform row. The live divider
// below remains the single boundary before the window-mode actions.
visible: false
width: parent ? parent.width : 0
height: 7
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: 1
color: theme.borderSubtle
}
}
Rectangle {
// The waveform seekbar is parked for a later release. Preserve this
// control and its preference wiring, but do not expose a dead toggle.
visible: false
width: parent ? parent.width : 0
height: 33
radius: 5
color: waveformArea.containsMouse ? theme.surfaceHover : "transparent"
Row {
anchors.fill: parent
anchors.leftMargin: 8
spacing: 8
QbzIcon {
name: "audio-lines"
width: 15
height: 15
anchors.verticalCenter: parent.verticalCenter
tintName: "secondary"
}
Text {
height: parent.height
width: parent.width - 23 - 22
text: QbzSession.tr("Track waveform", QbzSession.trRev)
color: theme.textSecondary
font.pixelSize: 13
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
QbzIcon {
visible: QbzShell.seekbarWaveform
name: "check"
width: 13
height: 13
anchors.verticalCenter: parent.verticalCenter
tintName: "accent"
}
}
MouseArea {
id: waveformArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.close()
QbzBridge.settingsBool("seekbar-waveform", !QbzShell.seekbarWaveform)
}
}
}
// Divider + the window-mode rows (Miniplayer / Immersive / Kiosk), all
// three live.
Item {
width: parent ? parent.width : 0
height: 7
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: 1
color: theme.borderSubtle
}
}
Repeater {
// Window-mode rows. `live` flips per-row inside the shared delegate
// (2026-08-02 immersive-port §7); Immersive went live in that
// contract's B2, Kiosk in the 2026-08-02-kiosk-port §8.1, Miniplayer
// in the 2026-08-03 miniplayer/tray-port §4.10 B2.
//
// Each row carries its own `action` because the delegate's MouseArea
// used to call QbzImmersive.openFromMenu() unconditionally: flipping
// any other row's `live` would have made it open Immersive.
//
// The Kiosk label mirrors the profile, 1:1 with the reference
// (`shell/PlayerBar.slint:797`): in kiosk it reads "Desktop mode",
// because the row is the way back out.
//
// THE MINIPLAYER ROW IS ABSENT UNDER THE KIOSK PROFILE, not dimmed —
// 1:1 with `crates/qbz-ui/ui/shell/PlayerBarSmall.slint:819`, which
// wraps its ContextMenuItem in `if !ShellState.kiosk-profile`. The
// reference's reason is the appliance shell: a second floating,
// draggable, always-on-top window has no way back on a touch-only
// kiosk. Building the model in JS (not a ternary inside one row) is
// what makes the row genuinely absent; the binding re-runs when
// QbzShell.kioskProfile changes, which the live desktop/kiosk toggle
// does. This closes ticket T-13 (contract §4.10.2): the kiosk contract
// landed first, so this diff owes the gate and pays it here.
model: {
var rows = []
if (!QbzShell.kioskProfile)
rows.push({ "label": QbzSession.tr("Miniplayer", QbzSession.trRev),
"icon": "picture-in-picture-2", "live": true, "action": "miniplayer" })
rows.push({ "label": QbzSession.tr("Immersive", QbzSession.trRev),
"icon": "maximize-2", "live": true, "action": "immersive" })
rows.push({ "label": QbzShell.kioskProfile
? QbzSession.tr("Desktop mode", QbzSession.trRev)
: QbzSession.tr("Kiosk mode", QbzSession.trRev),
"icon": "hard-drive", "live": true, "action": "kiosk" })
return rows
}
delegate: Rectangle {
required property var modelData
width: parent ? parent.width : 0
height: 33
radius: 5
opacity: modelData.live ? 1.0 : 0.45
color: (modelData.live && modeArea.containsMouse)
? theme.surfaceHover : "transparent"
Row {
anchors.fill: parent
anchors.leftMargin: 8
spacing: 8
QbzIcon { name: modelData.icon; width: 15; height: 15; anchors.verticalCenter: parent.verticalCenter; tintName: "secondary" }
Text {
height: parent.height
text: modelData.label
color: theme.textSecondary
font.pixelSize: 13
verticalAlignment: Text.AlignVCenter
}
}
MouseArea {
id: modeArea
anchors.fill: parent
enabled: modelData.live
hoverEnabled: true
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
root.close()
if (modelData.action === "immersive")
QbzImmersive.openFromMenu()
else if (modelData.action === "kiosk")
QbzSession.toggleProfile()
else if (modelData.action === "miniplayer")
// Suppressed under gamescope INSIDE enter() — one
// predicate for this row and for Shift+M (§4.10.1).
QbzMini.enter()
}
}
}
}
}