feat(qbz-qt): phase 18 — all four NPB modes + ViewMode flyout
PlayerBar.qml covers New/Classic/Large (full-width seekbar with times, responsive 3-column layout, glass Classic SongCard variant, right cluster), NowPlayingBar seam Loader picks per mode; Small unchanged. ViewMode flyout wired (New/Classic/Small/Large live, Miniplayer/ Immersive/Kiosk inert replicas), mode persisted to the shared ui_prefs npb_mode key, Large forces the sidebar open and docks the 208px cover (SidebarNowPlayingDock equivalent); AppShell bar height mode-aware (42/112). Empty and playing states verified per mode; user's persisted small pref restored after probes. km
No files matched your search
@@ -47,6 +47,9 @@ fn main() {
|
|||||||
"qml/Sidebar.qml",
|
"qml/Sidebar.qml",
|
||||||
"qml/HeaderBar.qml",
|
"qml/HeaderBar.qml",
|
||||||
"qml/NowPlayingBarSmall.qml",
|
"qml/NowPlayingBarSmall.qml",
|
||||||
|
"qml/NowPlayingBar.qml",
|
||||||
|
"qml/PlayerBar.qml",
|
||||||
|
"qml/ViewModeMenu.qml",
|
||||||
"qml/QueuePanel.qml",
|
"qml/QueuePanel.qml",
|
||||||
"qml/LyricsPanel.qml",
|
"qml/LyricsPanel.qml",
|
||||||
"qml/HomeView.qml",
|
"qml/HomeView.qml",
|
||||||
|
|||||||
@@ -58,13 +58,14 @@ Rectangle {
|
|||||||
// the compositor's business).
|
// the compositor's business).
|
||||||
}
|
}
|
||||||
|
|
||||||
NowPlayingBarSmall {
|
NowPlayingBar {
|
||||||
id: npb
|
id: npb
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
height: theme.npbSmallHeight
|
// Mode-aware height (AppShell.slint:396): Small collapses to one
|
||||||
// Square corners (see the header note above).
|
// header-tall row; New/Classic/Large keep the full 112px.
|
||||||
|
height: QbzBridge.npbMode === 2 ? theme.npbSmallHeight : 112
|
||||||
}
|
}
|
||||||
|
|
||||||
Sidebar {
|
Sidebar {
|
||||||
@@ -151,6 +152,54 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Large NPB (mode 3) cover dock (phase 18) -------------------------
|
||||||
|
// The L's vertical arm: the square now-playing cover pinned flush to
|
||||||
|
// the window bottom-left over the sidebar (SidebarNowPlayingDock.slint,
|
||||||
|
// AppShell.slint:747). Only while Large is ACTIVE (mode 3 + sidebar
|
||||||
|
// open). The spectrum band above the cover is NOT rendered — it needs
|
||||||
|
// the FFT visualizer tap, which the POC does not wire (POC-NOTE).
|
||||||
|
readonly property bool largeActive: QbzBridge.npbMode === 3 && QbzBridge.sidebarState === 0
|
||||||
|
Rectangle {
|
||||||
|
visible: root.largeActive
|
||||||
|
x: 16
|
||||||
|
y: parent.height - 221
|
||||||
|
width: 208
|
||||||
|
height: 221
|
||||||
|
color: "transparent"
|
||||||
|
// Drop-shadow approximation (the Slint dock's 24px blur).
|
||||||
|
Rectangle {
|
||||||
|
x: 0
|
||||||
|
y: 17
|
||||||
|
width: 208
|
||||||
|
height: 208
|
||||||
|
radius: theme.radiusMd
|
||||||
|
color: "#66000000"
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
x: 0
|
||||||
|
y: 13
|
||||||
|
width: 208
|
||||||
|
height: 208
|
||||||
|
radius: theme.radiusMd
|
||||||
|
color: ambientOn ? theme.surfaceMainA22 : theme.surfaceMain
|
||||||
|
clip: true
|
||||||
|
RoundedImage {
|
||||||
|
visible: QbzBridge.npHasTrack
|
||||||
|
anchors.fill: parent
|
||||||
|
source: QbzBridge.npArtworkPath
|
||||||
|
radius: theme.radiusMd
|
||||||
|
}
|
||||||
|
QbzIcon {
|
||||||
|
visible: !QbzBridge.npHasTrack
|
||||||
|
name: "music"
|
||||||
|
width: 66
|
||||||
|
height: 66
|
||||||
|
anchors.centerIn: parent
|
||||||
|
tintName: "muted"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Search cortinilla (phase 15): the live-search dropdown overlay, LAST
|
// Search cortinilla (phase 15): the live-search dropdown overlay, LAST
|
||||||
// child so it renders above every surface (Cortinilla.slint's mount).
|
// child so it renders above every surface (Cortinilla.slint's mount).
|
||||||
Cortinilla {
|
Cortinilla {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// NowPlayingBar SHELL — the mode seam (NowPlayingBar.slint, phase 18):
|
||||||
|
// mounts NowPlayingBarSmall for mode 2 (Small) and the full PlayerBar for
|
||||||
|
// modes 0 (New) / 1 (Classic) / 3 (Large). AppShell pins the height
|
||||||
|
// mode-aware (42px Small / 112px otherwise).
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import com.blitzfc.qbz
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
anchors.fill: parent
|
||||||
|
source: QbzBridge.npbMode === 2 ? "NowPlayingBarSmall.qml" : "PlayerBar.qml"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// Bottom now-playing / transport bar — QML port of
|
// Bottom now-playing / transport bar — QML port of
|
||||||
// crates/qbz-ui/ui/shell/PlayerBarSmall.slint (Mode C "Small", the only
|
// crates/qbz-ui/ui/shell/PlayerBarSmall.slint (Mode C "Small"). Mounted by
|
||||||
// variant in this POC).
|
// the NowPlayingBar seam when npbMode == 2 (phase 18).
|
||||||
//
|
//
|
||||||
// 3px full-width top seekbar (3-color: background / cache / progress),
|
// 3px full-width top seekbar (3-color: background / cache / progress),
|
||||||
// then the symmetric 3-column controls row (~39px):
|
// then the symmetric 3-column controls row (~39px):
|
||||||
@@ -406,7 +406,12 @@ Rectangle {
|
|||||||
BarIconBtn { name: "monitor-speaker" }
|
BarIconBtn { name: "monitor-speaker" }
|
||||||
BarIconBtn { name: "cast" }
|
BarIconBtn { name: "cast" }
|
||||||
BarIconBtn { name: "settings-2" }
|
BarIconBtn { name: "settings-2" }
|
||||||
BarIconBtn { name: "layout-grid" }
|
BarIconBtn {
|
||||||
|
id: smallViewBtn
|
||||||
|
name: "layout-grid"
|
||||||
|
active: smallViewMenu.opened
|
||||||
|
onClicked: smallViewMenu.openBelowRight(smallViewBtn)
|
||||||
|
}
|
||||||
BarIconBtn {
|
BarIconBtn {
|
||||||
name: "mic-vocal"
|
name: "mic-vocal"
|
||||||
active: QbzBridge.lyricsOpen
|
active: QbzBridge.lyricsOpen
|
||||||
@@ -468,4 +473,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The Now-Playing-view mode menu (phase 18 — shared with PlayerBar).
|
||||||
|
ViewModeMenu { id: smallViewMenu }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,615 @@
|
|||||||
|
// PlayerBar — the full 112px now-playing bar for modes 0 (New), 1
|
||||||
|
// (Classic) and 3 (Large) — QML port of crates/qbz-ui/ui/shell/
|
||||||
|
// PlayerBar.slint (phase 18). Mode 2 (Small) mounts NowPlayingBarSmall
|
||||||
|
// instead (the NowPlayingBar seam picks the body).
|
||||||
|
//
|
||||||
|
// Composition (1:1): a full-width SeekBar with times (6px margins; in
|
||||||
|
// Large the left edge insets by the sidebar width so the seek arm clears
|
||||||
|
// the docked cover), then the controls row in three responsive symmetric
|
||||||
|
// columns (the side columns share a fraction so PLAY lands on the window
|
||||||
|
// centre at every width):
|
||||||
|
// width < 1366 -> 39% | 22% | 39%
|
||||||
|
// 1366..<1920 -> 30% | 40% | 30%
|
||||||
|
// width >= 1920 -> 25% | 50% | 25%
|
||||||
|
// Classic uses a fixed 32.4/35.2/32.4 split (col-side 0.324).
|
||||||
|
//
|
||||||
|
// New (0): LEFT SongCard · CENTER transport (filled disc) · RIGHT cluster
|
||||||
|
// Classic (1): LEFT transport · CENTER glass SongCard (<=560px) · RIGHT cluster
|
||||||
|
// Large (3): LEFT SongCard WITHOUT the cover (it lives in the sidebar
|
||||||
|
// dock, shifted right to clear it) · CENTER transport ·
|
||||||
|
// RIGHT small AudioStamp + cluster.
|
||||||
|
//
|
||||||
|
// Wired: transport (shuffle/prev/play/next/repeat), seek, volume+mute,
|
||||||
|
// lyrics, queue, the Now-Playing-view flyout (npbSetMode), open
|
||||||
|
// album/artist via the SongCard meta links. Inert (POC-NOTEs): Connect
|
||||||
|
// (device flyout), track-info, the dot-LED output badges use the real
|
||||||
|
// backend state but the volume LOCK (ALSA hw / remote) is not enforced.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import com.blitzfc.qbz
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
color: ambientOn ? theme.surfaceCardA50 : theme.surfaceCard
|
||||||
|
readonly property bool ambientOn: QbzBridge.ambientMode > 0 && QbzBridge.npHasTrack
|
||||||
|
readonly property bool largeActive: QbzBridge.npbMode === 3 && QbzBridge.sidebarState === 0
|
||||||
|
readonly property bool isClassic: QbzBridge.npbMode === 1
|
||||||
|
|
||||||
|
QbzTheme { id: theme }
|
||||||
|
|
||||||
|
// Responsive zones (PlayerBar.slint). Classic fixes col-side at 0.324;
|
||||||
|
// New/Large use the width-driven side fraction. Both keep the two side
|
||||||
|
// columns EQUAL so the centre column stays dead centre.
|
||||||
|
property real sideFrac: root.width >= 1920 ? 0.25 : (root.width >= 1366 ? 0.30 : 0.39)
|
||||||
|
property real colSide: isClassic ? 0.324 : sideFrac
|
||||||
|
property real colCentre: 1.0 - 2.0 * colSide
|
||||||
|
// The docked-cover width the Large seek arm + song card must clear
|
||||||
|
// (sidebar rendered width; 240 when open).
|
||||||
|
readonly property int dockWidth: largeActive ? 240 : 0
|
||||||
|
|
||||||
|
readonly property var settingsDoc: parseSettings()
|
||||||
|
function parseSettings() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(QbzBridge.settingsJson)
|
||||||
|
} catch (e) {
|
||||||
|
return ({})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readonly property bool backendPipewire: settingsDoc.backendIsPipewire === true
|
||||||
|
readonly property bool dacPassthrough: settingsDoc.dacPassthrough === true
|
||||||
|
|
||||||
|
function fmt(secs) {
|
||||||
|
var m = Math.floor(secs / 60)
|
||||||
|
var s = Math.floor(secs % 60)
|
||||||
|
return m + ":" + (s < 10 ? "0" : "") + s
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Shared bits -------------------------------------------------------
|
||||||
|
|
||||||
|
// Square compact icon button (BarControls.IconButton).
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dot LED (circle-dot on / circle off) + constant-colour label — the
|
||||||
|
// New-mode output indicators (SongCard dot-leds).
|
||||||
|
component DotLed: Row {
|
||||||
|
property string label: ""
|
||||||
|
property bool on: false
|
||||||
|
spacing: 4
|
||||||
|
height: 12
|
||||||
|
Rectangle {
|
||||||
|
width: 8
|
||||||
|
height: 8
|
||||||
|
radius: 4
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: parent.on ? theme.accent : "transparent"
|
||||||
|
border.width: 1
|
||||||
|
border.color: parent.on ? theme.accent : theme.textMuted
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: parent.label
|
||||||
|
color: theme.textMuted
|
||||||
|
font.pixelSize: 9
|
||||||
|
font.weight: theme.weightSemibold
|
||||||
|
font.letterSpacing: 0.5
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The audio stamp (quality badge + dot LEDs) — New mode keeps the full
|
||||||
|
// stamp inside the SongCard; Large mounts a compact one on the right
|
||||||
|
// column's left wall.
|
||||||
|
component AudioStamp: Row {
|
||||||
|
property bool showLeds: true
|
||||||
|
spacing: 8
|
||||||
|
height: 30
|
||||||
|
layoutDirection: Qt.RightToLeft
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: 5
|
||||||
|
layoutDirection: Qt.RightToLeft
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
Text {
|
||||||
|
visible: QbzBridge.npQualityLabel !== ""
|
||||||
|
text: QbzBridge.npQualityLabel
|
||||||
|
font.pixelSize: 9
|
||||||
|
font.weight: theme.weightSemibold
|
||||||
|
color: theme.textPrimary
|
||||||
|
elide: Text.ElideRight
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
Image {
|
||||||
|
visible: QbzBridge.npQualityTier === "hires"
|
||||||
|
source: "assets/hi-res.svg"
|
||||||
|
width: 42
|
||||||
|
height: 28
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
sourceSize: Qt.size(84, 56)
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
visible: QbzBridge.npQualityTier !== "hires" && QbzBridge.npQualityLabel !== ""
|
||||||
|
width: 30
|
||||||
|
height: 30
|
||||||
|
radius: 3
|
||||||
|
color: theme.surfaceElevated
|
||||||
|
border.width: 1
|
||||||
|
border.color: theme.borderSubtle
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
QbzIcon { name: "cd"; width: 16; height: 16; anchors.centerIn: parent; tintName: "muted" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
visible: showLeds
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 1
|
||||||
|
DotLed { label: "PIPEWIRE"; on: root.backendPipewire }
|
||||||
|
DotLed { label: "DACPASS"; on: root.dacPassthrough }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SongCard (SongCard.slint): cover + title + context meta + the in-card
|
||||||
|
// audio stamp. `glass` renders the Classic contained card.
|
||||||
|
component SongCard: Rectangle {
|
||||||
|
property bool showArt: true
|
||||||
|
property bool showBadges: true
|
||||||
|
property bool glass: false
|
||||||
|
property int artSize: glass ? 60 : 74
|
||||||
|
width: 200
|
||||||
|
height: glass ? 64 : 74
|
||||||
|
radius: glass ? 8 : 0
|
||||||
|
color: ambientOn ? "transparent" : (glass ? theme.surfaceElevated : "transparent")
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: stamp.left
|
||||||
|
anchors.rightMargin: 4
|
||||||
|
height: parent.height
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
visible: showArt
|
||||||
|
width: showArt ? artSize : 0
|
||||||
|
height: parent.height
|
||||||
|
Rectangle {
|
||||||
|
width: artSize
|
||||||
|
height: artSize
|
||||||
|
anchors.centerIn: parent
|
||||||
|
radius: 6
|
||||||
|
color: theme.surfaceElevated
|
||||||
|
clip: true
|
||||||
|
RoundedImage {
|
||||||
|
visible: QbzBridge.npHasTrack
|
||||||
|
anchors.fill: parent
|
||||||
|
source: QbzBridge.npArtworkPath
|
||||||
|
radius: 6
|
||||||
|
}
|
||||||
|
QbzIcon {
|
||||||
|
visible: !QbzBridge.npHasTrack
|
||||||
|
name: "music"
|
||||||
|
width: artSize * 0.5
|
||||||
|
height: artSize * 0.5
|
||||||
|
anchors.centerIn: parent
|
||||||
|
tintName: "muted"
|
||||||
|
}
|
||||||
|
// Fetch overlay (resolving/downloading).
|
||||||
|
Rectangle {
|
||||||
|
visible: QbzBridge.npHasTrack && QbzBridge.npLoading
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: 6
|
||||||
|
color: "#aa000000"
|
||||||
|
QbzSpinner {
|
||||||
|
width: artSize * 0.5
|
||||||
|
height: artSize * 0.5
|
||||||
|
anchors.centerIn: parent
|
||||||
|
size: artSize * 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item { width: showArt ? 11 : 0; height: 1 }
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width - (showArt ? artSize + 11 : 0)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 4
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: QbzBridge.npHasTrack ? QbzBridge.npTitle : QbzBridge.tr("Nothing playing")
|
||||||
|
color: theme.textPrimary
|
||||||
|
font.pixelSize: theme.fontBody
|
||||||
|
font.weight: theme.weightMedium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
visible: QbzBridge.npHasTrack
|
||||||
|
spacing: 7
|
||||||
|
height: 18
|
||||||
|
// Context-stack icon ("Show track playing context" pref).
|
||||||
|
Rectangle {
|
||||||
|
visible: QbzBridge.showContextIcon
|
||||||
|
width: 16
|
||||||
|
height: 18
|
||||||
|
radius: 3
|
||||||
|
color: ctxArea.containsMouse ? theme.surfaceHover : "transparent"
|
||||||
|
QbzIcon {
|
||||||
|
name: "layers"
|
||||||
|
width: 13
|
||||||
|
height: 13
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
tintName: ctxArea.containsMouse ? "primary" : "muted"
|
||||||
|
}
|
||||||
|
MouseArea {
|
||||||
|
id: ctxArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: if (QbzBridge.npAlbumId !== "") QbzBridge.openAlbum(QbzBridge.npAlbumId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: QbzBridge.npArtist
|
||||||
|
color: artistArea.containsMouse && QbzBridge.npArtistId !== "" ? theme.accent : theme.textMuted
|
||||||
|
font.pixelSize: 12
|
||||||
|
elide: Text.ElideRight
|
||||||
|
MouseArea {
|
||||||
|
id: artistArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: QbzBridge.npArtistId !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
onClicked: if (QbzBridge.npArtistId !== "") QbzBridge.openArtist(QbzBridge.npArtistId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
width: 3
|
||||||
|
height: 3
|
||||||
|
radius: 1.5
|
||||||
|
color: theme.textMuted
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: QbzBridge.npAlbum
|
||||||
|
color: albumArea.containsMouse && QbzBridge.npAlbumId !== "" ? theme.accent : theme.textMuted
|
||||||
|
font.pixelSize: 12
|
||||||
|
elide: Text.ElideRight
|
||||||
|
MouseArea {
|
||||||
|
id: albumArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: QbzBridge.npAlbumId !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
onClicked: if (QbzBridge.npAlbumId !== "") QbzBridge.openAlbum(QbzBridge.npAlbumId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AudioStamp {
|
||||||
|
id: stamp
|
||||||
|
visible: showBadges && QbzBridge.npHasTrack
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transport cluster (TransportControls.slint): shuffle · prev · play ·
|
||||||
|
// next · repeat. play-circle = the filled 44px accent disc (New/Large);
|
||||||
|
// classic-actions = plain glyphs, left-aligned (Classic).
|
||||||
|
component TransportControls: Row {
|
||||||
|
property bool playCircle: true
|
||||||
|
spacing: playCircle ? 12 : 4
|
||||||
|
height: 44
|
||||||
|
|
||||||
|
BarIconBtn {
|
||||||
|
name: "shuffle"
|
||||||
|
active: QbzBridge.npShuffle
|
||||||
|
btnEnabled: QbzBridge.npHasTrack
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: QbzBridge.toggleShuffle()
|
||||||
|
}
|
||||||
|
BarIconBtn {
|
||||||
|
name: "skip-back"
|
||||||
|
btnEnabled: QbzBridge.npHasTrack
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: QbzBridge.previous()
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
width: playCircle ? 44 : 32
|
||||||
|
height: playCircle ? 44 : 32
|
||||||
|
radius: width / 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
opacity: QbzBridge.npHasTrack ? 1.0 : 0.3
|
||||||
|
color: playCircle ? (playArea.containsMouse && QbzBridge.npHasTrack ? theme.accentHover : theme.accent)
|
||||||
|
: ((playArea.containsMouse && QbzBridge.npHasTrack) ? theme.surfaceHover : "transparent")
|
||||||
|
QbzIcon {
|
||||||
|
name: QbzBridge.npPlaying ? "pause" : "play-fill"
|
||||||
|
width: playCircle ? 20 : 16
|
||||||
|
height: playCircle ? 20 : 16
|
||||||
|
anchors.centerIn: parent
|
||||||
|
tintName: playCircle ? "black" : "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
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: QbzBridge.next()
|
||||||
|
}
|
||||||
|
BarIconBtn {
|
||||||
|
name: QbzBridge.npRepeatMode === 2 ? "repeat-1" : "repeat"
|
||||||
|
active: QbzBridge.npRepeatMode > 0
|
||||||
|
btnEnabled: QbzBridge.npHasTrack
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: QbzBridge.cycleRepeat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ the bar =================================
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Item { width: 1; height: 6 }
|
||||||
|
|
||||||
|
// Full-width seek bar (times at the ends). In Large the seek arm is
|
||||||
|
// LEFT-INSET by the docked-cover width so it begins right of the
|
||||||
|
// dock (AppShell.slint's large-active padding).
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 20
|
||||||
|
Text {
|
||||||
|
x: root.dockWidth > 0 ? root.dockWidth + 6 : 6
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: QbzBridge.npHasTrack
|
||||||
|
text: root.fmt(QbzBridge.npElapsedSecs)
|
||||||
|
color: theme.textMuted
|
||||||
|
font.pixelSize: 11
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
id: seekTrack
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.leftMargin: (root.dockWidth > 0 ? root.dockWidth + 6 : 6) + (QbzBridge.npHasTrack ? 46 : 0)
|
||||||
|
anchors.rightMargin: 6 + (QbzBridge.npHasTrack ? 46 : 0)
|
||||||
|
height: 4
|
||||||
|
radius: 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: theme.surfaceElevated
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width * Math.min(Math.max(QbzBridge.npCacheProgress, 0), 1)
|
||||||
|
height: parent.height
|
||||||
|
radius: 2
|
||||||
|
color: theme.borderMuted
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width * Math.min(Math.max(QbzBridge.npProgress, 0), 1)
|
||||||
|
height: parent.height
|
||||||
|
radius: 2
|
||||||
|
color: theme.accent
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
visible: QbzBridge.npHasTrack
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 6
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: QbzBridge.npHasTrack
|
||||||
|
text: "-" + root.fmt(Math.max(0, QbzBridge.npDurationSecs - QbzBridge.npElapsedSecs))
|
||||||
|
color: theme.textMuted
|
||||||
|
font.pixelSize: 11
|
||||||
|
}
|
||||||
|
MouseArea {
|
||||||
|
anchors.left: seekTrack.left
|
||||||
|
anchors.right: seekTrack.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
cursorShape: QbzBridge.npHasTrack ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
onPressed: if (QbzBridge.npHasTrack) QbzBridge.seek(Math.min(Math.max(mouseX / width, 0), 1))
|
||||||
|
onPositionChanged: if (pressed && QbzBridge.npHasTrack) QbzBridge.seek(Math.min(Math.max(mouseX / width, 0), 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { width: 1; height: 6 }
|
||||||
|
|
||||||
|
// --- Controls: the responsive symmetric zones -----------------------
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - 38
|
||||||
|
|
||||||
|
// LEFT column.
|
||||||
|
Item {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 6
|
||||||
|
width: (parent.width - 12) * root.colSide
|
||||||
|
height: parent.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
// New (0) AND Large (3): the song card (Large drops the cover
|
||||||
|
// — it lives in the dock — and shifts right to clear it).
|
||||||
|
SongCard {
|
||||||
|
visible: !root.isClassic
|
||||||
|
x: root.largeActive ? root.dockWidth + 8 : 0
|
||||||
|
width: parent.width - x
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
showArt: !root.largeActive
|
||||||
|
showBadges: !root.largeActive
|
||||||
|
}
|
||||||
|
// Classic: transport cluster hugging the left edge (plain
|
||||||
|
// play glyph, Tauri arrangement).
|
||||||
|
TransportControls {
|
||||||
|
visible: root.isClassic
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
playCircle: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CENTRE column.
|
||||||
|
Item {
|
||||||
|
x: 6 + (parent.width - 12) * root.colSide
|
||||||
|
width: (parent.width - 12) * root.colCentre
|
||||||
|
height: parent.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
// New (0) AND Large (3): centred transport, PLAY on the
|
||||||
|
// window centre.
|
||||||
|
TransportControls {
|
||||||
|
visible: !root.isClassic
|
||||||
|
anchors.centerIn: parent
|
||||||
|
playCircle: true
|
||||||
|
}
|
||||||
|
// Classic: the contained glass song card (<=560px cap).
|
||||||
|
SongCard {
|
||||||
|
visible: root.isClassic
|
||||||
|
glass: true
|
||||||
|
width: Math.min(parent.width, 560)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RIGHT column — secondary actions + volume, clustered at the
|
||||||
|
// right wall.
|
||||||
|
Item {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 6
|
||||||
|
width: (parent.width - 12) * root.colSide
|
||||||
|
height: parent.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
// Large: the SMALL AudioStamp just LEFT of the icon
|
||||||
|
// cluster at a fixed 12px gap (the badges move out of the
|
||||||
|
// song card in Large).
|
||||||
|
AudioStamp {
|
||||||
|
visible: root.largeActive && QbzBridge.npHasTrack
|
||||||
|
showLeds: false
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
Item { visible: root.largeActive; width: 12; height: 1 }
|
||||||
|
|
||||||
|
// Qobuz Connect — inert (device flyout out of scope).
|
||||||
|
BarIconBtn { name: "element-connect" }
|
||||||
|
|
||||||
|
// Now-Playing view — the mode flyout (phase 18).
|
||||||
|
BarIconBtn {
|
||||||
|
name: "layout-grid"
|
||||||
|
active: viewMenu.opened
|
||||||
|
onClicked: viewMenu.openBelowRight(viewBtn)
|
||||||
|
id: viewBtn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lyrics.
|
||||||
|
BarIconBtn {
|
||||||
|
name: "mic-vocal"
|
||||||
|
active: QbzBridge.lyricsOpen
|
||||||
|
onClicked: QbzBridge.toggleLyrics()
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { width: 6; height: 1 }
|
||||||
|
|
||||||
|
// Volume.
|
||||||
|
BarIconBtn {
|
||||||
|
name: QbzBridge.npMuted ? "volume-x" : "volume-2"
|
||||||
|
active: QbzBridge.npMuted
|
||||||
|
onClicked: QbzBridge.toggleMute()
|
||||||
|
}
|
||||||
|
Item {
|
||||||
|
width: 81
|
||||||
|
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: 6; height: 1 }
|
||||||
|
|
||||||
|
// Queue panel toggle.
|
||||||
|
BarIconBtn {
|
||||||
|
name: "list-ordered"
|
||||||
|
active: QbzBridge.queueOpen
|
||||||
|
onClicked: QbzBridge.toggleQueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { width: 1; height: 6 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Now-Playing-view mode menu (shared with the Small bar).
|
||||||
|
ViewModeMenu { id: viewMenu }
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
// 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
|
||||||
|
// inert window-mode rows (Miniplayer / Immersive / Kiosk — the Slint has
|
||||||
|
// them; the POC has no such surfaces, POC-NOTE).
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import com.blitzfc.qbz
|
||||||
|
|
||||||
|
QbzContextMenu {
|
||||||
|
id: root
|
||||||
|
menuWidth: 196
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: [
|
||||||
|
{ "label": QbzBridge.tr("New"), "icon": "panel-left", "mode": 0 },
|
||||||
|
{ "label": QbzBridge.tr("Classic"), "icon": "panel-right-close", "mode": 1 },
|
||||||
|
{ "label": QbzBridge.tr("Small"), "icon": "rows-3", "mode": 2 },
|
||||||
|
{ "label": QbzBridge.tr("Large"), "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: QbzBridge.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()
|
||||||
|
QbzBridge.npbSetMode(modelData.mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Divider + the window-mode rows (Miniplayer / Immersive / Kiosk —
|
||||||
|
// present in the Slint menu; inert in the POC (no such surfaces).
|
||||||
|
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 {
|
||||||
|
model: [
|
||||||
|
{ "label": QbzBridge.tr("Miniplayer"), "icon": "picture-in-picture-2" },
|
||||||
|
{ "label": QbzBridge.tr("Immersive"), "icon": "maximize-2" },
|
||||||
|
{ "label": QbzBridge.tr("Kiosk mode"), "icon": "hard-drive" },
|
||||||
|
]
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
width: parent ? parent.width : 0
|
||||||
|
height: 33
|
||||||
|
radius: 5
|
||||||
|
opacity: 0.45
|
||||||
|
color: "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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Inert (POC-NOTE): no miniplayer/immersive/kiosk surfaces.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#4285f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 9V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h4" />
|
||||||
|
<rect width="10" height="7" x="12" y="13" rx="2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 310 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#4285f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||||
|
<path d="M21 9H3" />
|
||||||
|
<path d="M21 15H3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 284 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 9V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h4" />
|
||||||
|
<rect width="10" height="7" x="12" y="13" rx="2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 310 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||||
|
<path d="M21 9H3" />
|
||||||
|
<path d="M21 15H3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 284 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 9V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h4" />
|
||||||
|
<rect width="10" height="7" x="12" y="13" rx="2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 310 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||||
|
<path d="M21 9H3" />
|
||||||
|
<path d="M21 15H3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 284 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#888888" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 9V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h4" />
|
||||||
|
<rect width="10" height="7" x="12" y="13" rx="2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 310 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#888888" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||||
|
<path d="M21 9H3" />
|
||||||
|
<path d="M21 15H3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 284 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 9V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h4" />
|
||||||
|
<rect width="10" height="7" x="12" y="13" rx="2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 310 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||||
|
<path d="M21 9H3" />
|
||||||
|
<path d="M21 15H3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 284 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#cccccc" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 9V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h4" />
|
||||||
|
<rect width="10" height="7" x="12" y="13" rx="2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 310 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#cccccc" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||||
|
<path d="M21 9H3" />
|
||||||
|
<path d="M21 15H3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 284 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fbbf24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 9V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h4" />
|
||||||
|
<rect width="10" height="7" x="12" y="13" rx="2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 310 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fbbf24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||||
|
<path d="M21 9H3" />
|
||||||
|
<path d="M21 15H3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 284 B |
@@ -126,6 +126,15 @@ pub mod qbz_bridge {
|
|||||||
#[qproperty(QString, view_param_id)]
|
#[qproperty(QString, view_param_id)]
|
||||||
// Now-playing track id (playing-row indicator in track lists).
|
// Now-playing track id (playing-row indicator in track lists).
|
||||||
#[qproperty(QString, np_track_id)]
|
#[qproperty(QString, np_track_id)]
|
||||||
|
#[qproperty(QString, np_album)]
|
||||||
|
#[qproperty(QString, np_album_id)]
|
||||||
|
#[qproperty(QString, np_artist_id)]
|
||||||
|
// Now-playing bar mode (phase 18): 0 New / 1 Classic / 2 Small /
|
||||||
|
// 3 Large — the ui_prefs npb_mode key, live-switchable.
|
||||||
|
#[qproperty(i32, npb_mode)]
|
||||||
|
// "Show track playing context" pref (Playback settings) — feeds the
|
||||||
|
// SongCard layers icon.
|
||||||
|
#[qproperty(bool, show_context_icon)]
|
||||||
|
|
||||||
// --- Lyrics panel (phase 9) ----------------------------------------
|
// --- Lyrics panel (phase 9) ----------------------------------------
|
||||||
#[qproperty(bool, lyrics_open)]
|
#[qproperty(bool, lyrics_open)]
|
||||||
@@ -414,6 +423,11 @@ pub mod qbz_bridge {
|
|||||||
/// falling back to the studio discography (playback.rs play_artist).
|
/// falling back to the studio discography (playback.rs play_artist).
|
||||||
#[qinvokable]
|
#[qinvokable]
|
||||||
fn play_artist_card(self: Pin<&mut QbzBridge>, artist_id: QString);
|
fn play_artist_card(self: Pin<&mut QbzBridge>, artist_id: QString);
|
||||||
|
/// Now-Playing-view flyout: switch the bar mode (0 New / 1 Classic /
|
||||||
|
/// 2 Small / 3 Large) — persists ui_prefs.npb_mode; Large forces the
|
||||||
|
/// sidebar open (the Slint "large" arm).
|
||||||
|
#[qinvokable]
|
||||||
|
fn npb_set_mode(self: Pin<&mut QbzBridge>, mode: i32);
|
||||||
/// Emitted with the next page of a releases bucket.
|
/// Emitted with the next page of a releases bucket.
|
||||||
#[qsignal]
|
#[qsignal]
|
||||||
fn release_section_ready(self: Pin<&mut QbzBridge>, release_type: QString, cards_json: QString, has_more: bool);
|
fn release_section_ready(self: Pin<&mut QbzBridge>, release_type: QString, cards_json: QString, has_more: bool);
|
||||||
@@ -612,6 +626,11 @@ pub struct QbzBridgeRust {
|
|||||||
artist_json: QString,
|
artist_json: QString,
|
||||||
view_param_id: QString,
|
view_param_id: QString,
|
||||||
np_track_id: QString,
|
np_track_id: QString,
|
||||||
|
np_album: QString,
|
||||||
|
np_album_id: QString,
|
||||||
|
np_artist_id: QString,
|
||||||
|
npb_mode: i32,
|
||||||
|
show_context_icon: bool,
|
||||||
queue_json: QString,
|
queue_json: QString,
|
||||||
lyrics_open: bool,
|
lyrics_open: bool,
|
||||||
lyrics_json: QString,
|
lyrics_json: QString,
|
||||||
@@ -699,6 +718,11 @@ impl Default for QbzBridgeRust {
|
|||||||
artist_json: QString::from("{}"),
|
artist_json: QString::from("{}"),
|
||||||
view_param_id: QString::default(),
|
view_param_id: QString::default(),
|
||||||
np_track_id: QString::default(),
|
np_track_id: QString::default(),
|
||||||
|
np_album: QString::default(),
|
||||||
|
np_album_id: QString::default(),
|
||||||
|
np_artist_id: QString::default(),
|
||||||
|
npb_mode: crate::settings_qt::npb_mode_index(),
|
||||||
|
show_context_icon: crate::settings_qt::show_context_icon(),
|
||||||
queue_json: QString::from("{}"),
|
queue_json: QString::from("{}"),
|
||||||
lyrics_open: false,
|
lyrics_open: false,
|
||||||
lyrics_json: QString::from("{}"),
|
lyrics_json: QString::from("{}"),
|
||||||
@@ -983,6 +1007,10 @@ impl qbz_bridge::QbzBridge {
|
|||||||
crate::play_artist_card(artist_id.to_string());
|
crate::play_artist_card(artist_id.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn npb_set_mode(self: Pin<&mut Self>, mode: i32) {
|
||||||
|
crate::npb_set_mode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn settings_bool(self: Pin<&mut Self>, key: QString, value: bool) {
|
pub fn settings_bool(self: Pin<&mut Self>, key: QString, value: bool) {
|
||||||
crate::settings_bool(key.to_string(), value);
|
crate::settings_bool(key.to_string(), value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -801,6 +801,19 @@ pub(crate) fn playlist_set_follow_by_id(playlist_id: u64, follow: bool) {
|
|||||||
spawn(async move { playlist_qt::set_follow_by_id(&runtime, playlist_id, follow).await });
|
spawn(async move { playlist_qt::set_follow_by_id(&runtime, playlist_id, follow).await });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Now-Playing-view flyout: switch + persist the bar mode. Large forces
|
||||||
|
/// the sidebar open (the Slint "large" arm — the dock needs it).
|
||||||
|
pub(crate) fn npb_set_mode(mode: i32) {
|
||||||
|
let mode = settings_qt::set_npb_mode(mode);
|
||||||
|
log::info!("[qbz-qt] npb_mode -> {mode}");
|
||||||
|
ui(move |mut b| {
|
||||||
|
if mode == 3 {
|
||||||
|
b.as_mut().set_sidebar_state(0);
|
||||||
|
}
|
||||||
|
b.as_mut().set_npb_mode(mode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Artist-card overlay play (ArtistGridCard): Popular tracks with the
|
/// Artist-card overlay play (ArtistGridCard): Popular tracks with the
|
||||||
/// studio-discography fallback.
|
/// studio-discography fallback.
|
||||||
pub(crate) fn play_artist_card(artist_id: String) {
|
pub(crate) fn play_artist_card(artist_id: String) {
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ pub struct NowPlayingModel {
|
|||||||
pub has_track: bool,
|
pub has_track: bool,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub artist: String,
|
pub artist: String,
|
||||||
|
pub album: String,
|
||||||
|
pub album_id: String,
|
||||||
|
pub artist_id: String,
|
||||||
pub artwork_path: String,
|
pub artwork_path: String,
|
||||||
pub elapsed_secs: i32,
|
pub elapsed_secs: i32,
|
||||||
pub duration_secs: i32,
|
pub duration_secs: i32,
|
||||||
@@ -40,6 +43,9 @@ impl Default for NowPlayingModel {
|
|||||||
has_track: false,
|
has_track: false,
|
||||||
title: String::new(),
|
title: String::new(),
|
||||||
artist: String::new(),
|
artist: String::new(),
|
||||||
|
album: String::new(),
|
||||||
|
album_id: String::new(),
|
||||||
|
artist_id: String::new(),
|
||||||
artwork_path: String::new(),
|
artwork_path: String::new(),
|
||||||
elapsed_secs: 0,
|
elapsed_secs: 0,
|
||||||
duration_secs: 0,
|
duration_secs: 0,
|
||||||
@@ -60,6 +66,9 @@ static MODEL: Mutex<NowPlayingModel> = Mutex::new(NowPlayingModel {
|
|||||||
has_track: false,
|
has_track: false,
|
||||||
title: String::new(),
|
title: String::new(),
|
||||||
artist: String::new(),
|
artist: String::new(),
|
||||||
|
album: String::new(),
|
||||||
|
album_id: String::new(),
|
||||||
|
artist_id: String::new(),
|
||||||
artwork_path: String::new(),
|
artwork_path: String::new(),
|
||||||
elapsed_secs: 0,
|
elapsed_secs: 0,
|
||||||
duration_secs: 0,
|
duration_secs: 0,
|
||||||
@@ -92,6 +101,11 @@ fn publish(m: &NowPlayingModel) {
|
|||||||
b.as_mut().set_np_has_track(m.has_track);
|
b.as_mut().set_np_has_track(m.has_track);
|
||||||
b.as_mut().set_np_title(QString::from(m.title.as_str()));
|
b.as_mut().set_np_title(QString::from(m.title.as_str()));
|
||||||
b.as_mut().set_np_artist(QString::from(m.artist.as_str()));
|
b.as_mut().set_np_artist(QString::from(m.artist.as_str()));
|
||||||
|
b.as_mut().set_np_album(QString::from(m.album.as_str()));
|
||||||
|
b.as_mut()
|
||||||
|
.set_np_album_id(QString::from(m.album_id.as_str()));
|
||||||
|
b.as_mut()
|
||||||
|
.set_np_artist_id(QString::from(m.artist_id.as_str()));
|
||||||
b.as_mut()
|
b.as_mut()
|
||||||
.set_np_artwork_path(QString::from(m.artwork_path.as_str()));
|
.set_np_artwork_path(QString::from(m.artwork_path.as_str()));
|
||||||
b.as_mut().set_np_elapsed_secs(m.elapsed_secs);
|
b.as_mut().set_np_elapsed_secs(m.elapsed_secs);
|
||||||
@@ -157,6 +171,9 @@ pub fn set_playing(playing: bool) {
|
|||||||
pub struct TrackMeta {
|
pub struct TrackMeta {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub artist: String,
|
pub artist: String,
|
||||||
|
pub album: String,
|
||||||
|
pub album_id: String,
|
||||||
|
pub artist_id: String,
|
||||||
pub duration_secs: i32,
|
pub duration_secs: i32,
|
||||||
pub quality_tier: String,
|
pub quality_tier: String,
|
||||||
pub quality_label: String,
|
pub quality_label: String,
|
||||||
@@ -172,6 +189,9 @@ pub fn set_track(meta: TrackMeta) {
|
|||||||
m.has_track = true;
|
m.has_track = true;
|
||||||
m.title = meta.title;
|
m.title = meta.title;
|
||||||
m.artist = meta.artist;
|
m.artist = meta.artist;
|
||||||
|
m.album = meta.album;
|
||||||
|
m.album_id = meta.album_id;
|
||||||
|
m.artist_id = meta.artist_id;
|
||||||
m.duration_secs = meta.duration_secs;
|
m.duration_secs = meta.duration_secs;
|
||||||
m.elapsed_secs = 0;
|
m.elapsed_secs = 0;
|
||||||
m.quality_tier = meta.quality_tier;
|
m.quality_tier = meta.quality_tier;
|
||||||
@@ -242,6 +262,11 @@ fn publish_with_cache(m: &NowPlayingModel) {
|
|||||||
b.as_mut().set_np_has_track(m.has_track);
|
b.as_mut().set_np_has_track(m.has_track);
|
||||||
b.as_mut().set_np_title(QString::from(m.title.as_str()));
|
b.as_mut().set_np_title(QString::from(m.title.as_str()));
|
||||||
b.as_mut().set_np_artist(QString::from(m.artist.as_str()));
|
b.as_mut().set_np_artist(QString::from(m.artist.as_str()));
|
||||||
|
b.as_mut().set_np_album(QString::from(m.album.as_str()));
|
||||||
|
b.as_mut()
|
||||||
|
.set_np_album_id(QString::from(m.album_id.as_str()));
|
||||||
|
b.as_mut()
|
||||||
|
.set_np_artist_id(QString::from(m.artist_id.as_str()));
|
||||||
b.as_mut()
|
b.as_mut()
|
||||||
.set_np_artwork_path(QString::from(m.artwork_path.as_str()));
|
.set_np_artwork_path(QString::from(m.artwork_path.as_str()));
|
||||||
b.as_mut().set_np_elapsed_secs(m.elapsed_secs);
|
b.as_mut().set_np_elapsed_secs(m.elapsed_secs);
|
||||||
|
|||||||
@@ -613,6 +613,12 @@ pub(crate) async fn refresh_now_playing(runtime: &Arc<AppRuntime<LoggingAdapter>
|
|||||||
crate::now_playing::set_track(crate::now_playing::TrackMeta {
|
crate::now_playing::set_track(crate::now_playing::TrackMeta {
|
||||||
title,
|
title,
|
||||||
artist: track.artist.clone(),
|
artist: track.artist.clone(),
|
||||||
|
album: track.album.clone(),
|
||||||
|
album_id: track.album_id.clone().unwrap_or_default(),
|
||||||
|
artist_id: track
|
||||||
|
.artist_id
|
||||||
|
.map(|id| id.to_string())
|
||||||
|
.unwrap_or_default(),
|
||||||
duration_secs: track.duration_secs as i32,
|
duration_secs: track.duration_secs as i32,
|
||||||
quality_tier: tier,
|
quality_tier: tier,
|
||||||
quality_label: label,
|
quality_label: label,
|
||||||
|
|||||||
@@ -224,6 +224,66 @@ pub fn toggle_ambient_background() -> i32 {
|
|||||||
if next { 1 } else { 0 }
|
if next { 1 } else { 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ui_prefs.json (now-playing bar mode, phase 18) — the Slint `npb_mode` key:
|
||||||
|
// "new" | "classic" | "small" | "large" (ui_prefs.rs:357-360; maps to
|
||||||
|
// ShellState.npb-mode 0/1/2/3). Same additive key patch.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// The persisted mode as a bridge index (unknown keys fall back to "new").
|
||||||
|
pub fn npb_mode_index() -> i32 {
|
||||||
|
let Some(path) = prefs_path() else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let key = std::fs::read_to_string(path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|text| serde_json::from_str::<serde_json::Value>(&text).ok())
|
||||||
|
.and_then(|v| v.get("npb_mode").and_then(|q| q.as_str().map(str::to_string)))
|
||||||
|
.unwrap_or_else(|| "new".to_string());
|
||||||
|
match key.as_str() {
|
||||||
|
"classic" => 1,
|
||||||
|
"small" => 2,
|
||||||
|
"large" => 3,
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn npb_mode_key(index: i32) -> &'static str {
|
||||||
|
match index {
|
||||||
|
1 => "classic",
|
||||||
|
2 => "small",
|
||||||
|
3 => "large",
|
||||||
|
_ => "new",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persist a mode index (0-3) to the shared key; returns the index.
|
||||||
|
pub fn set_npb_mode(index: i32) -> i32 {
|
||||||
|
let index = index.clamp(0, 3);
|
||||||
|
if let Some(path) = prefs_path() {
|
||||||
|
let mut value: serde_json::Value = std::fs::read_to_string(&path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|text| serde_json::from_str(&text).ok())
|
||||||
|
.unwrap_or_else(|| serde_json::json!({}));
|
||||||
|
if let Some(obj) = value.as_object_mut() {
|
||||||
|
obj.insert(
|
||||||
|
"npb_mode".to_string(),
|
||||||
|
serde_json::Value::String(npb_mode_key(index).to_string()),
|
||||||
|
);
|
||||||
|
if let Ok(text) = serde_json::to_string_pretty(&value) {
|
||||||
|
let _ = std::fs::write(&path, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The "Show track playing context" pref (Playback settings; feeds the
|
||||||
|
/// SongCard layers icon — SettingsState.show-context-icon).
|
||||||
|
pub fn show_context_icon() -> bool {
|
||||||
|
with_playback(|s| s.get_preferences().map(|p| p.show_context_icon)).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
pub const STREAMING_QUALITY_KEYS: &[&str] = &["mp3", "cd", "hires", "hires_plus"];
|
pub const STREAMING_QUALITY_KEYS: &[&str] = &["mp3", "cd", "hires", "hires_plus"];
|
||||||
pub const STREAMING_QUALITY_LABELS: &[&str] = &["MP3", "CD Quality", "Hi-Res", "Hi-Res+"];
|
pub const STREAMING_QUALITY_LABELS: &[&str] = &["MP3", "CD Quality", "Hi-Res", "Hi-Res+"];
|
||||||
|
|
||||||
|
|||||||