Files
qbz/crates/qbz-qt/qml/NowPlayingBarSmall.qml
T
vicrodh b10ff98620 feat(qbz-qt): phase 9 — Queue homologation + Lyrics side panel
Queue panel 1:1: History tab (click replays per queue.rs semantics),
#442 'Next in queue'/'Next up' section markers from manual_next_count,
40-row pagination + live search + page clamp, row actions (play,
remove, remove-all-after, heart), press-drag reorder via move_track,
clear queue, plural-aware footer count, exact empty states. New Lyrics
panel: qbz-lyrics service + per-user cache wired, Qobuz-first chain
with LRCLIB fallback, synced line view with line-level highlight +
centered auto-follow + click-to-seek, exact status bodies. Both panels
stack Feishin-style in the right column; NPB lyrics button wired.

km
2026-07-27 18:22:22 -06:00

469 lines
19 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Bottom now-playing / transport bar — QML port of
// crates/qbz-ui/ui/shell/PlayerBarSmall.slint (Mode C "Small", the only
// variant in this POC).
//
// 3px full-width top seekbar (3-color: background / cache / progress),
// then the symmetric 3-column controls row (~39px):
// LEFT — 37px album art + 2-line title/artist + fixed time column
// CENTER — transport cluster (info · shuffle · prev · play · next ·
// repeat · +)
// RIGHT — AudioStamp · Connect · Cast · Settings · ViewMode · Lyrics ·
// Volume · Queue
// Total height 42px (ShellState.npb-small-height; npb-small-extra is 0 on
// Linux).
//
// POC-NOTE: playback is not wired (phase 4) — the bar renders its EMPTY
// state (NowPlayingState.has-track == false): idle music-glyph art,
// "Nothing playing" title, transport disabled (opacity 0.3), no time
// column, no AudioStamp. Connect / Cast / Settings / ViewMode / Lyrics are
// inert visual replicas (their flyouts are out of scope); Volume, Queue,
// Shuffle, Repeat, Mute are live against the POC NowPlayingModel.
import QtQuick
import com.blitzfc.qbz
Rectangle {
id: root
color: theme.surfaceCard
QbzTheme { id: theme }
// Responsive side fraction — IDENTICAL to PlayerBarSmall's mechanism:
// LEFT and RIGHT columns share this fraction so the CENTER transport
// column lands PLAY on the window centre.
// width < 1366 -> 39% | 22% | 39%
// 1366..<1920 -> 30% | 40% | 30%
// width >= 1920 -> 25% | 50% | 25%
property real sideFrac: root.width >= 1920 ? 0.25 : (root.width >= 1366 ? 0.30 : 0.39)
property real colCentre: 1.0 - 2.0 * root.sideFrac
// WIDE = inline horizontal volume slider; NARROW = button-only.
property bool volumeWide: root.width >= 1366
function fmt(secs) {
var m = Math.floor(secs / 60)
var s = Math.floor(secs % 60)
return m + ":" + (s < 10 ? "0" : "") + s
}
// Square compact icon button (BarControls.IconButton): disabled = dim
// 0.3 + non-clickable; active = accent tint.
component BarIconBtn: Rectangle {
property string name: ""
property int iconSize: 16
property bool active: false
property bool btnEnabled: true
signal clicked()
width: 32
height: 32
radius: theme.radiusSm
opacity: btnEnabled ? 1.0 : 0.3
color: (biArea.containsMouse && btnEnabled) ? theme.surfaceHover : "transparent"
QbzIcon {
name: parent.name
width: parent.iconSize
height: parent.iconSize
anchors.centerIn: parent
tintName: parent.active ? "accent"
: (biArea.containsMouse && parent.btnEnabled)
? "primary" : "secondary"
}
MouseArea {
id: biArea
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.btnEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: if (parent.btnEnabled) parent.clicked()
}
}
// Thin vertical separator (PlayerBarSmall's Sep): a 1px line,
// border-subtle, ~60% of the row height, vertically centred; the
// horizontal gaps are the caller's.
component Sep: Item {
property int gapLeft: 6
property int gapRight: 6
width: gapLeft + 1 + gapRight
height: parent ? parent.height : 0
Rectangle {
x: gapLeft
width: 1
height: 24
anchors.verticalCenter: parent.verticalCenter
color: theme.borderSubtle
}
}
Column {
anchors.fill: parent
spacing: 0
// === A. TOP full-width seekbar (the content/bar divider) ========
Item {
width: parent.width
height: 3
Rectangle {
id: seekTrack
width: parent.width
height: 3
radius: 2
color: theme.surfaceElevated
// Buffered / cache line.
Rectangle {
width: parent.width * Math.min(Math.max(QbzBridge.npCacheProgress, 0), 1)
height: parent.height
radius: 2
color: theme.textMuted
opacity: 0.35
}
// Playback progress line.
Rectangle {
width: parent.width * Math.min(Math.max(QbzBridge.npProgress, 0), 1)
height: parent.height
radius: 2
color: theme.accent
}
// Hover thumb.
Rectangle {
width: 12
height: 12
radius: 6
color: theme.textPrimary
x: parent.width * Math.min(Math.max(QbzBridge.npProgress, 0), 1) - width / 2
anchors.verticalCenter: parent.verticalCenter
opacity: seekArea.containsMouse ? 1.0 : 0.0
Behavior on opacity { NumberAnimation { duration: 100 } }
}
}
// Tall, easy-to-grab hit area over the thin line.
MouseArea {
id: seekArea
width: parent.width
height: 18
anchors.verticalCenter: seekTrack.verticalCenter
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: QbzBridge.npHasTrack
onClicked: QbzBridge.seek(Math.min(Math.max(mouseX / width, 0), 1))
}
}
// === B. CONTROLS ROW — symmetric 3 columns =======================
Item {
width: parent.width
height: parent.height - 3
// ---- LEFT column: art + title/artist + time column ---------
Item {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: (root.width - 6) * root.sideFrac
clip: true
Row {
anchors.fill: parent
spacing: 0
// Song card: 37px art (3px surface-card border, flush to
// the window's left edge) + tight 2-line text block.
// Width = the column minus the Sep + time column, so the
// title elides at exactly the free space.
Item {
height: parent.height
width: parent.width - (QbzBridge.npHasTrack ? 59 : 0)
Rectangle {
id: artBox
width: 37
height: 37
anchors.verticalCenter: parent.verticalCenter
radius: 6
border.width: 3
border.color: theme.surfaceCard
color: theme.surfaceElevated
clip: true
Image {
visible: QbzBridge.npHasTrack && QbzBridge.npArtworkPath !== ""
anchors.fill: parent
source: QbzBridge.npArtworkPath
fillMode: Image.PreserveAspectCrop
}
// Idle (no track): a STATIC album glyph
// placeholder (50% of the art size, muted).
QbzIcon {
visible: !QbzBridge.npHasTrack
name: "music"
width: 18.5
height: 18.5
anchors.centerIn: parent
tintName: "muted"
}
}
// Title + artist — constrained to the art's inner
// height (37 − 2×3 = 31px) and vertically centred
// (SongCard text-center).
Column {
id: metaBlock
width: Math.max(0, parent.width - 37 - 9)
height: 31
anchors.left: artBox.right
anchors.leftMargin: 9
anchors.verticalCenter: parent.verticalCenter
spacing: 1
Text {
width: parent.width
text: QbzBridge.npHasTrack
? QbzBridge.npTitle : QbzBridge.tr("Nothing playing")
color: theme.textPrimary
font.pixelSize: 13
font.weight: theme.weightMedium
elide: Text.ElideRight
}
Text {
visible: QbzBridge.npHasTrack
width: parent.width
text: QbzBridge.npArtist
color: theme.textMuted
font.pixelSize: 11
elide: Text.ElideRight
}
}
}
// Separator: song-card | time column (8px toward the
// card, 10px toward the timer).
Sep {
visible: QbzBridge.npHasTrack
gapLeft: 8
gapRight: 10
}
// Time column — fixed 40px 2-row block (elapsed /
// total), monospace for stable digit positions.
Column {
visible: QbzBridge.npHasTrack
width: 40
anchors.verticalCenter: parent.verticalCenter
spacing: 1
Text {
text: root.fmt(QbzBridge.npElapsedSecs)
font.family: "monospace"
font.pixelSize: 10
color: theme.textMuted
}
Text {
text: root.fmt(QbzBridge.npDurationSecs)
font.family: "monospace"
font.pixelSize: 10
color: theme.textMuted
}
}
}
}
// ---- CENTER column: transport, window-centred --------------
Item {
anchors.left: parent.left
anchors.leftMargin: (root.width - 6) * root.sideFrac
anchors.top: parent.top
anchors.bottom: parent.bottom
width: (root.width - 6) * root.colCentre
clip: true
Row {
anchors.centerIn: parent
spacing: 2
BarIconBtn {
name: "info"
btnEnabled: QbzBridge.npHasTrack
// POC-NOTE: track-info panel is out of scope.
}
BarIconBtn {
name: "shuffle"
active: QbzBridge.npShuffle
btnEnabled: QbzBridge.npHasTrack
onClicked: QbzBridge.toggleShuffle()
}
BarIconBtn {
name: "skip-back"
btnEnabled: QbzBridge.npHasTrack
onClicked: QbzBridge.previous()
}
// Play/pause — the Classic plain glyph (play-circle:
// false in Small): 34px, 20px glyph, text-primary,
// accent on hover.
Rectangle {
width: 34
height: 34
radius: theme.radiusSm
opacity: QbzBridge.npHasTrack ? 1.0 : 0.3
color: (playArea.containsMouse && QbzBridge.npHasTrack)
? theme.surfaceHover : "transparent"
QbzIcon {
name: QbzBridge.npPlaying ? "pause" : "play-fill"
width: 20
height: 20
anchors.centerIn: parent
tintName: playArea.containsMouse ? "accent" : "primary"
}
MouseArea {
id: playArea
anchors.fill: parent
hoverEnabled: true
cursorShape: QbzBridge.npHasTrack ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: if (QbzBridge.npHasTrack) QbzBridge.togglePlay()
}
}
BarIconBtn {
name: "skip-forward"
btnEnabled: QbzBridge.npHasTrack
onClicked: QbzBridge.next()
}
BarIconBtn {
name: QbzBridge.npRepeatMode === 2 ? "repeat-1" : "repeat"
active: QbzBridge.npRepeatMode !== 0
btnEnabled: QbzBridge.npHasTrack
onClicked: QbzBridge.cycleRepeat()
}
BarIconBtn {
name: "plus"
btnEnabled: QbzBridge.npHasTrack
// POC-NOTE: the grouped "Add to…" flyout is out of
// scope.
}
}
}
// ---- RIGHT column: the control SET, right-aligned ----------
// Order (PlayerBarSmall mount order): AudioStamp · Connect ·
// Cast · Settings · ViewMode · Lyrics · Volume · Queue.
Item {
anchors.right: parent.right
anchors.rightMargin: 6
anchors.top: parent.top
anchors.bottom: parent.bottom
width: (root.width - 6) * root.sideFrac
clip: true
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: parent.height
spacing: 1
// AudioStamp — tier + detail line, width-clamped.
// POC-NOTE: row 2 (backend/mode LEDs) is not rendered —
// the POC model carries no output-backend state.
Column {
visible: QbzBridge.npHasTrack
anchors.verticalCenter: parent.verticalCenter
spacing: 1
Row {
spacing: 5
layoutDirection: Qt.RightToLeft
Text {
text: QbzBridge.npQualityLabel
font.pixelSize: 9
font.weight: theme.weightSemibold
color: theme.textPrimary
elide: Text.ElideRight
}
Text {
text: QbzBridge.npQualityTier === "hires" ? "HI-RES"
: QbzBridge.npQualityTier === "mp3" ? "MP3"
: QbzBridge.npQualityTier === "lossless" ? "LOSSLESS" : "CD"
font.pixelSize: 9
font.weight: theme.weightBold
font.letterSpacing: 0.3
color: theme.textSecondary
}
}
}
// Separator: AudioStamp | icon cluster (10px toward the
// stamp, 8px toward the buttons).
Sep {
visible: QbzBridge.npHasTrack
gapLeft: 10
gapRight: 8
}
// POC-NOTE: Connect / Cast / Settings / ViewMode /
// Lyrics are inert visual replicas — their flyouts
// (device pickers, audio toggles, NPB-mode menu) are
// out of scope for the POC.
BarIconBtn { name: "monitor-speaker" }
BarIconBtn { name: "cast" }
BarIconBtn { name: "settings-2" }
BarIconBtn { name: "layout-grid" }
BarIconBtn {
name: "mic-vocal"
active: QbzBridge.lyricsOpen
onClicked: QbzBridge.toggleLyrics()
}
Item { width: 4; height: 1 }
// Volume — mute icon + (WIDE) inline horizontal slider.
BarIconBtn {
name: QbzBridge.npMuted ? "volume-x" : "volume-2"
active: QbzBridge.npMuted
onClicked: QbzBridge.toggleMute()
}
Item {
visible: root.volumeWide
width: 72
height: 32
Rectangle {
id: volTrack
width: parent.width
height: 4
radius: 2
anchors.verticalCenter: parent.verticalCenter
color: theme.surfaceElevated
Rectangle {
width: parent.width * QbzBridge.npVolume
height: parent.height
radius: 2
color: theme.accent
}
Rectangle {
width: 16
height: 16
radius: 8
color: theme.textPrimary
x: parent.width * QbzBridge.npVolume - width / 2
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onPressed: QbzBridge.setVolume(Math.min(Math.max(mouseX / width, 0), 1))
onPositionChanged: if (pressed)
QbzBridge.setVolume(Math.min(Math.max(mouseX / width, 0), 1))
}
}
Item { width: 4; height: 1 }
// Queue panel toggle.
BarIconBtn {
name: "list-ordered"
active: QbzBridge.queueOpen
onClicked: QbzBridge.toggleQueue()
}
}
}
}
}
}