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/HeaderBar.qml",
|
||||
"qml/NowPlayingBarSmall.qml",
|
||||
"qml/NowPlayingBar.qml",
|
||||
"qml/PlayerBar.qml",
|
||||
"qml/ViewModeMenu.qml",
|
||||
"qml/QueuePanel.qml",
|
||||
"qml/LyricsPanel.qml",
|
||||
"qml/HomeView.qml",
|
||||
|
||||
@@ -58,13 +58,14 @@ Rectangle {
|
||||
// the compositor's business).
|
||||
}
|
||||
|
||||
NowPlayingBarSmall {
|
||||
NowPlayingBar {
|
||||
id: npb
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: theme.npbSmallHeight
|
||||
// Square corners (see the header note above).
|
||||
// Mode-aware height (AppShell.slint:396): Small collapses to one
|
||||
// header-tall row; New/Classic/Large keep the full 112px.
|
||||
height: QbzBridge.npbMode === 2 ? theme.npbSmallHeight : 112
|
||||
}
|
||||
|
||||
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
|
||||
// child so it renders above every surface (Cortinilla.slint's mount).
|
||||
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
|
||||
// crates/qbz-ui/ui/shell/PlayerBarSmall.slint (Mode C "Small", the only
|
||||
// variant in this POC).
|
||||
// crates/qbz-ui/ui/shell/PlayerBarSmall.slint (Mode C "Small"). Mounted by
|
||||
// the NowPlayingBar seam when npbMode == 2 (phase 18).
|
||||
//
|
||||
// 3px full-width top seekbar (3-color: background / cache / progress),
|
||||
// then the symmetric 3-column controls row (~39px):
|
||||
@@ -406,7 +406,12 @@ Rectangle {
|
||||
BarIconBtn { name: "monitor-speaker" }
|
||||
BarIconBtn { name: "cast" }
|
||||
BarIconBtn { name: "settings-2" }
|
||||
BarIconBtn { name: "layout-grid" }
|
||||
BarIconBtn {
|
||||
id: smallViewBtn
|
||||
name: "layout-grid"
|
||||
active: smallViewMenu.opened
|
||||
onClicked: smallViewMenu.openBelowRight(smallViewBtn)
|
||||
}
|
||||
BarIconBtn {
|
||||
name: "mic-vocal"
|
||||
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)]
|
||||
// Now-playing track id (playing-row indicator in track lists).
|
||||
#[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) ----------------------------------------
|
||||
#[qproperty(bool, lyrics_open)]
|
||||
@@ -414,6 +423,11 @@ pub mod qbz_bridge {
|
||||
/// falling back to the studio discography (playback.rs play_artist).
|
||||
#[qinvokable]
|
||||
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.
|
||||
#[qsignal]
|
||||
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,
|
||||
view_param_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,
|
||||
lyrics_open: bool,
|
||||
lyrics_json: QString,
|
||||
@@ -699,6 +718,11 @@ impl Default for QbzBridgeRust {
|
||||
artist_json: QString::from("{}"),
|
||||
view_param_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("{}"),
|
||||
lyrics_open: false,
|
||||
lyrics_json: QString::from("{}"),
|
||||
@@ -983,6 +1007,10 @@ impl qbz_bridge::QbzBridge {
|
||||
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) {
|
||||
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 });
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// studio-discography fallback.
|
||||
pub(crate) fn play_artist_card(artist_id: String) {
|
||||
|
||||
@@ -16,6 +16,9 @@ pub struct NowPlayingModel {
|
||||
pub has_track: bool,
|
||||
pub title: String,
|
||||
pub artist: String,
|
||||
pub album: String,
|
||||
pub album_id: String,
|
||||
pub artist_id: String,
|
||||
pub artwork_path: String,
|
||||
pub elapsed_secs: i32,
|
||||
pub duration_secs: i32,
|
||||
@@ -40,6 +43,9 @@ impl Default for NowPlayingModel {
|
||||
has_track: false,
|
||||
title: String::new(),
|
||||
artist: String::new(),
|
||||
album: String::new(),
|
||||
album_id: String::new(),
|
||||
artist_id: String::new(),
|
||||
artwork_path: String::new(),
|
||||
elapsed_secs: 0,
|
||||
duration_secs: 0,
|
||||
@@ -60,6 +66,9 @@ static MODEL: Mutex<NowPlayingModel> = Mutex::new(NowPlayingModel {
|
||||
has_track: false,
|
||||
title: String::new(),
|
||||
artist: String::new(),
|
||||
album: String::new(),
|
||||
album_id: String::new(),
|
||||
artist_id: String::new(),
|
||||
artwork_path: String::new(),
|
||||
elapsed_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_title(QString::from(m.title.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()
|
||||
.set_np_artwork_path(QString::from(m.artwork_path.as_str()));
|
||||
b.as_mut().set_np_elapsed_secs(m.elapsed_secs);
|
||||
@@ -157,6 +171,9 @@ pub fn set_playing(playing: bool) {
|
||||
pub struct TrackMeta {
|
||||
pub title: String,
|
||||
pub artist: String,
|
||||
pub album: String,
|
||||
pub album_id: String,
|
||||
pub artist_id: String,
|
||||
pub duration_secs: i32,
|
||||
pub quality_tier: String,
|
||||
pub quality_label: String,
|
||||
@@ -172,6 +189,9 @@ pub fn set_track(meta: TrackMeta) {
|
||||
m.has_track = true;
|
||||
m.title = meta.title;
|
||||
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.elapsed_secs = 0;
|
||||
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_title(QString::from(m.title.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()
|
||||
.set_np_artwork_path(QString::from(m.artwork_path.as_str()));
|
||||
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 {
|
||||
title,
|
||||
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,
|
||||
quality_tier: tier,
|
||||
quality_label: label,
|
||||
|
||||
@@ -224,6 +224,66 @@ pub fn toggle_ambient_background() -> i32 {
|
||||
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_LABELS: &[&str] = &["MP3", "CD Quality", "Hi-Res", "Hi-Res+"];
|
||||
|
||||
|
||||