feat(qbz-qt): now-playing bar buttons 1:1 with the Tauri original

Both bars are brought to the reference's control set, order, geometry and
hover treatment, and the bar's quality readout now mounts QualityBadge in its
compact mode instead of a hand-drawn stamp.

settings_qt publishes `normalization` in the settings document. It was already
settable through settingsBool but never read back, so both bars had to shadow
the toggle in local state and would drift from the real value after any change
made elsewhere.

cc
This commit is contained in:
vicrodh committed 2026-07-28 13:48:16 -06:00
1 parent 10461bb49c
commit f44f03e414
3 files changed
+398 -147

No files matched your search

+130 -71
View File
@@ -12,12 +12,16 @@
// Total height 42px (ShellState.npb-small-height; npb-small-extra is 0 on
// Linux).
//
// POC-NOTE: playback is not wired (phase 4) — the bar renders its EMPTY
// state (NowPlayingState.has-track == false): idle music-glyph art,
// "Nothing playing" title, transport disabled (opacity 0.3), no time
// column, no AudioStamp. Connect / Cast / Settings / ViewMode / Lyrics are
// inert visual replicas (their flyouts are out of scope); Volume, Queue,
// Shuffle, Repeat, Mute are live against the POC NowPlayingModel.
// BUTTON SET (phase 24): identical to PlayerBar's, in the Small bar's own
// order — the transport is the shared cluster (info · shuffle · prev · play ·
// next · repeat · "+"), and the right cluster keeps PlayerBarSmall's mount
// order (Connect · Cast · Settings · ViewMode · Lyrics · Volume · Queue).
// The "+" opens the same "Add to…" flyout as the full bar; the quality stamp
// is the shared QualityBadge in "compact" mode, not hand-drawn text.
//
// Connect / Cast / Settings / ViewMode are still inert visual replicas where
// no Qt invokable exists (TODO comments at the call sites); Volume, Queue,
// Shuffle, Repeat, Mute, Lyrics and the add flyout are live.
import QtQuick
import com.blitzfc.qbz
@@ -45,6 +49,32 @@ Rectangle {
// WIDE = inline horizontal volume slider; NARROW = button-only.
property bool volumeWide: root.width >= 1366
// Favorite state of the now-playing track (Slint:
// QueueState.now-playing-favorite) — the queue document carries it on its
// `current` row; re-parsed only when that document changes.
readonly property bool npFavorite: {
try {
var d = JSON.parse(QbzQueue.queueJson)
return !!(d && d.current && d.current.isFavorite)
} catch (e) {
return false
}
}
// np_quality_label is "24-bit / 96 kHz" (playback_qt::quality_badge);
// QualityBadge wants the raw numbers.
readonly property int npBitDepth: {
var l = QbzPlayer.npQualityLabel
var i = l.indexOf("-bit")
return i > 0 ? parseInt(l.substring(0, i)) : 0
}
readonly property real npSampleRate: {
var l = QbzPlayer.npQualityLabel
var i = l.indexOf("/")
var j = l.indexOf("kHz")
return (i >= 0 && j > i) ? parseFloat(l.substring(i + 1, j)) : 0
}
function fmt(secs) {
var m = Math.floor(secs / 60)
var s = Math.floor(secs % 60)
@@ -260,7 +290,8 @@ Rectangle {
QbzIconButton {
name: "info"
btnEnabled: QbzPlayer.npHasTrack
// POC-NOTE: track-info panel is out of scope.
// TODO(qt-bridge): no track-info panel in the Qt port
// yet (Slint: media-action(track, id, "track-info")).
}
QbzIconButton {
name: "shuffle"
@@ -309,11 +340,13 @@ Rectangle {
btnEnabled: QbzPlayer.npHasTrack
onClicked: QbzPlayer.cycleRepeat()
}
// "+" — Tauri's add-to-playlist button, grouped into the
// shared "Add to…" flyout in 2.0.
QbzIconButton {
id: smallAddBtn
name: "plus"
btnEnabled: QbzPlayer.npHasTrack
// POC-NOTE: the grouped "Add to…" flyout is out of
// scope.
onClicked: smallAddMenu.openBelowRight(smallAddBtn)
}
}
}
@@ -335,33 +368,18 @@ Rectangle {
height: parent.height
spacing: 1
// AudioStamp — tier + detail line, width-clamped.
// POC-NOTE: row 2 (backend/mode LEDs) is not rendered —
// the POC model carries no output-backend state.
Column {
visible: QbzPlayer.npHasTrack
// AudioStamp — the shared QualityBadge in its narrow-bar
// "compact" mode (icon + "24/96"), same component the full
// bar uses. POC-NOTE: row 2 (backend/mode LEDs) is not
// rendered here — the Small stamp has no room for it.
QualityBadge {
visible: QbzPlayer.npHasTrack && QbzPlayer.npQualityLabel !== ""
anchors.verticalCenter: parent.verticalCenter
spacing: 1
Row {
spacing: 5
layoutDirection: Qt.RightToLeft
Text {
text: QbzPlayer.npQualityLabel
font.pixelSize: 9
font.weight: theme.weightSemibold
color: theme.textPrimary
elide: Text.ElideRight
}
Text {
text: QbzPlayer.npQualityTier === "hires" ? "HI-RES"
: QbzPlayer.npQualityTier === "mp3" ? "MP3"
: QbzPlayer.npQualityTier === "lossless" ? "LOSSLESS" : "CD"
font.pixelSize: 9
font.weight: theme.weightBold
font.letterSpacing: 0.3
color: theme.textSecondary
}
}
mode: "compact"
tierOverride: QbzPlayer.npQualityTier === "hires" ? "hires"
: (QbzPlayer.npQualityLabel !== "" ? "cd" : "")
bitDepth: root.npBitDepth
samplingRate: root.npSampleRate
}
// Separator: AudioStamp | icon cluster (10px toward the
@@ -372,13 +390,32 @@ Rectangle {
gapRight: 8
}
// POC-NOTE: Connect / Cast / Settings / ViewMode /
// Lyrics are inert visual replicas — their flyouts
// (device pickers, audio toggles, NPB-mode menu) are
// out of scope for the POC.
// Qobuz Connect.
// TODO(qt-bridge): no qconnect state/toggle exposed
// (Slint: NowPlayingState.qconnect-connected +
// qconnect-toggle()). Rendered 1:1, inert.
QbzIconButton { name: "monitor-speaker" }
// Cast (Chromecast / DLNA).
// TODO(qt-bridge): no cast picker in the Qt port
// (Slint: CastState.picker-open + CastActions.open()).
QbzIconButton { name: "cast" }
QbzIconButton { name: "settings-2" }
// Audio settings — Tauri's normalization toggle (2.0
// groups normalization + gapless behind this button).
// TODO(glue): `normalization` is settable through
// settingsBool but NOT published in settingsJson, so the
// state can only be tracked locally; TODO(qt-bridge): the
// Slint two-toggle flyout is not ported — the click
// toggles normalization, exactly like the Tauri button.
QbzIconButton {
id: smallNormBtn
property bool normOn: false
name: "settings-2"
active: normOn
onClicked: {
normOn = !normOn
QbzBridge.settingsBool("normalization", normOn)
}
}
QbzIconButton {
id: smallViewBtn
name: "layout-grid"
@@ -399,39 +436,16 @@ Rectangle {
active: QbzPlayer.npMuted
onClicked: QbzPlayer.toggleMute()
}
Item {
// WIDE: the shared QbzSlider (PlayerBarSmall mounts the
// same primitive at 72px on the 0..1000 scale).
QbzSlider {
visible: root.volumeWide
width: 72
height: 32
Rectangle {
id: volTrack
width: parent.width
height: 4
radius: 2
anchors.verticalCenter: parent.verticalCenter
color: theme.surfaceElevated
Rectangle {
width: parent.width * QbzPlayer.npVolume
height: parent.height
radius: 2
color: theme.accent
}
Rectangle {
width: 16
height: 16
radius: 8
color: theme.textPrimary
x: parent.width * QbzPlayer.npVolume - width / 2
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onPressed: QbzPlayer.setVolume(Math.min(Math.max(mouseX / width, 0), 1))
onPositionChanged: if (pressed)
QbzPlayer.setVolume(Math.min(Math.max(mouseX / width, 0), 1))
}
anchors.verticalCenter: parent.verticalCenter
minimum: 0
maximum: 1000
value: Math.round(QbzPlayer.npVolume * 1000)
onChanged: function (v) { QbzPlayer.setVolume(v / 1000.0) }
}
Item { width: 4; height: 1 }
@@ -449,4 +463,49 @@ Rectangle {
// The Now-Playing-view mode menu (phase 18 — shared with PlayerBar).
ViewModeMenu { id: smallViewMenu }
// "Add to…" flyout behind the transport "+" (TransportControls.slint's
// add-menu) — same seven entries, order and icons as the full bar.
CardMenu {
id: smallAddMenu
menuWidth: 232
entries: {
var m = [
{
"label": root.npFavorite
? QbzSession.tr("Remove from library", QbzSession.trRev)
: QbzSession.tr("Add to library", QbzSession.trRev),
"icon": root.npFavorite ? "heart-filled" : "heart",
"action": "favorite"
},
{ "label": QbzSession.tr("Add to playlist", QbzSession.trRev), "icon": "list-music", "action": "playlist" },
{ "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "action": "queue" },
{ "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "action": "later" },
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "action": "next" },
{ "label": QbzSession.tr("Add to mixtape", QbzSession.trRev), "icon": "cassette-tape", "action": "mixtape" },
]
if (QbzPlayer.npAlbumId !== "") {
m.push({
"label": QbzSession.tr("Add album to collection", QbzSession.trRev),
"icon": "disc-3",
"action": "album-favorite"
})
}
return m
}
onPicked: function (a) {
var id = QbzPlayer.npTrackId
if (a === "favorite") {
if (id !== "") QbzQueue.queueToggleFavorite("track", id)
} else if (a === "queue" || a === "later" || a === "next") {
if (id !== "") QbzPlayer.enqueueTrack(id, a)
} else if (a === "album-favorite") {
if (QbzPlayer.npAlbumId !== "")
QbzBridge.libraryToggleFavorite("album", QbzPlayer.npAlbumId)
}
// TODO(qt-bridge): "playlist" (add-to-playlist modal) and
// "mixtape" have no invokable in the Qt port yet — the rows are
// rendered 1:1 with the Slint flyout and do nothing for now.
}
}
}
+264 -76
View File
@@ -19,11 +19,23 @@
// 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.
// BUTTON SET (phase 24 — 1:1 with the Tauri NowPlayingBar.svelte, via the
// Slint TransportControls/PlayerBar that descend from it):
// transport: info · shuffle · prev · play · next · repeat · "+" (Add to…)
// (+ the inline favorite heart in Classic, like Tauri)
// right: Cast · Connect · Lyrics · Now-Playing view · Audio settings
// (normalization) · [6px] · Mute · slider · −/+ steppers ·
// [6px] · Queue
// Tauri's Miniplayer + Full screen buttons live inside the Now-Playing view
// flyout (ViewModeMenu) since 2.0 — that ONE button holds both.
//
// Wired: transport (shuffle/prev/play/next/repeat), the add flyout
// (library / queue / play next / play later / album), seek, volume + mute +
// steppers, lyrics, queue, the Now-Playing-view flyout (npbSetMode),
// normalization, open album/artist via the SongCard meta links.
// Inert (TODO comments at the call sites): Cast, Connect (device flyouts),
// track-info, add-to-playlist, add-to-mixtape. The volume LOCK (ALSA hw /
// remote) is still not enforced.
import QtQuick
import QtQuick.Controls
@@ -60,6 +72,52 @@ Rectangle {
}
readonly property bool backendPipewire: settingsDoc.backendIsPipewire === true
readonly property bool dacPassthrough: settingsDoc.dacPassthrough === true
// AppearanceState.show-volume-steppers (PlayerBar.slint gates the −/+
// pair on it; Tauri always showed them).
readonly property bool showVolumeSteppers: settingsDoc.showVolumeSteppers === true
// Favorite state of the now-playing track (Slint:
// QueueState.now-playing-favorite). The queue document carries it on its
// `current` row; this re-parses ONLY when that document changes.
readonly property bool npFavorite: {
try {
var d = JSON.parse(QbzQueue.queueJson)
return !!(d && d.current && d.current.isFavorite)
} catch (e) {
return false
}
}
// Audio settings / normalization (Tauri's normalization button; Slint's
// "Audio settings" flyout). `normalization` IS settable through
// QbzBridge.settingsBool but is NOT published in settingsJson yet, so the
// toggle keeps its own value once the user touches it and falls back to
// the (currently absent) published field before that.
// TODO(glue): publish `normalization` in SettingsDoc — see the report.
property bool normTouched: false
property bool normLocal: false
readonly property bool normalizationOn: normTouched ? normLocal
: (settingsDoc.normalization === true)
function toggleNormalization() {
normLocal = !normalizationOn
normTouched = true
QbzBridge.settingsBool("normalization", normLocal)
}
// np_quality_label is "24-bit / 96 kHz" (playback_qt::quality_badge);
// QualityBadge wants the raw numbers, so split the published string
// instead of duplicating the tier logic.
readonly property int npBitDepth: {
var l = QbzPlayer.npQualityLabel
var i = l.indexOf("-bit")
return i > 0 ? parseInt(l.substring(0, i)) : 0
}
readonly property real npSampleRate: {
var l = QbzPlayer.npQualityLabel
var i = l.indexOf("/")
var j = l.indexOf("kHz")
return (i >= 0 && j > i) ? parseFloat(l.substring(i + 1, j)) : 0
}
function fmt(secs) {
var m = Math.floor(secs / 60)
@@ -107,26 +165,18 @@ Rectangle {
height: 30
layoutDirection: Qt.RightToLeft
Row {
spacing: 5
layoutDirection: Qt.RightToLeft
// The badge itself is the Tauri QualityBadge in its narrow-bar
// "compact" mode (icon + "24/96"), NOT a hand-drawn glyph+label pair.
QualityBadge {
visible: QbzPlayer.npQualityLabel !== ""
mode: "compact"
// The stamp shows the cd glyph for ANY non-hires tier with a
// label (not just "cd") — map that exactly.
tierOverride: QbzPlayer.npQualityTier === "hires" ? "hires"
: (QbzPlayer.npQualityLabel !== "" ? "cd" : "")
bitDepth: root.npBitDepth
samplingRate: root.npSampleRate
anchors.verticalCenter: parent.verticalCenter
Text {
visible: QbzPlayer.npQualityLabel !== ""
text: QbzPlayer.npQualityLabel
font.pixelSize: 9
font.weight: theme.weightSemibold
color: theme.textPrimary
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
}
QualityMini {
// The stamp shows the cd glyph for ANY non-hires tier with
// a label (not just "cd") — map that exactly.
tier: QbzPlayer.npQualityTier === "hires" ? "hires"
: (QbzPlayer.npQualityLabel !== "" ? "cd" : "")
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
visible: showLeds
@@ -282,14 +332,61 @@ Rectangle {
}
}
// 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).
// Favorite toggle — the inline heart Classic mounts next to "+" (Tauri's
// `.control-btn` heart with fill=currentColor when favorited). NOT a
// QbzIconButton: that control only tints accent/primary/secondary/muted
// and the favorited heart must take the theme's `favorite` red (Slint
// TransportControls uses Theme.favorite the same way).
component FavToggle: Rectangle {
width: 32
height: 32
radius: theme.radiusSm
opacity: QbzPlayer.npHasTrack ? 1.0 : 0.3
color: (favArea.containsMouse && QbzPlayer.npHasTrack) ? theme.surfaceHover : "transparent"
QbzIcon {
name: root.npFavorite ? "heart-filled" : "heart"
width: 16
height: 16
anchors.centerIn: parent
tintName: root.npFavorite ? "favorite"
: (favArea.containsMouse ? "primary" : "secondary")
}
MouseArea {
id: favArea
anchors.fill: parent
hoverEnabled: true
cursorShape: QbzPlayer.npHasTrack ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: if (QbzPlayer.npHasTrack && QbzPlayer.npTrackId !== "")
QbzQueue.queueToggleFavorite("track", QbzPlayer.npTrackId)
}
}
// Transport cluster (TransportControls.slint): track-info · shuffle ·
// prev · play · next · repeat · "+" (Add to… flyout), plus the inline
// favorite heart in Classic. 2px spacing on 32px buttons = the same 34px
// pitch as Tauri's 30px `.control-btn` + 4px gap.
// play-circle = the filled accent disc (New/Large); Classic/Small get the
// plain 34px glyph, 20px in BOTH modes exactly like Tauri's size={20}.
component TransportControls: Row {
id: tc
property bool playCircle: true
spacing: playCircle ? 12 : 4
// Classic ADDS the inline favorite toggle (Tauri parity).
property bool classicActions: false
/// Emitted by "+" with the button as anchor, so the flyout is owned
/// by the bar (one menu, both mount points).
signal addRequested(var anchorItem)
spacing: 2
height: 44
// Track info — the first control, left of shuffle (1:1 with the
// other views; it also restores PLAY's symmetric centring).
QbzIconButton {
name: "info"
btnEnabled: QbzPlayer.npHasTrack
anchors.verticalCenter: parent.verticalCenter
// TODO(qt-bridge): no track-info panel in the Qt port yet
// (Slint: media-action("track", id, "track-info")). Inert.
}
QbzIconButton {
name: "shuffle"
active: QbzPlayer.npShuffle
@@ -304,19 +401,21 @@ Rectangle {
onClicked: QbzPlayer.previous()
}
Rectangle {
width: playCircle ? 44 : 32
height: playCircle ? 44 : 32
radius: width / 2
width: tc.playCircle ? 38 : 34
height: tc.playCircle ? 38 : 34
radius: tc.playCircle ? width / 2 : theme.radiusSm
anchors.verticalCenter: parent.verticalCenter
opacity: QbzPlayer.npHasTrack ? 1.0 : 0.3
color: playCircle ? (playArea.containsMouse && QbzPlayer.npHasTrack ? theme.accentHover : theme.accent)
color: tc.playCircle
? (playArea.containsMouse && QbzPlayer.npHasTrack ? theme.accentHover : theme.accent)
: ((playArea.containsMouse && QbzPlayer.npHasTrack) ? theme.surfaceHover : "transparent")
QbzIcon {
name: QbzPlayer.npPlaying ? "pause" : "play-fill"
width: playCircle ? 20 : 16
height: playCircle ? 20 : 16
width: 20
height: 20
anchors.centerIn: parent
tintName: playCircle ? "black" : "primary"
tintName: tc.playCircle ? "black"
: (playArea.containsMouse ? "accent" : "primary")
}
MouseArea {
id: playArea
@@ -339,6 +438,19 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
onClicked: QbzPlayer.cycleRepeat()
}
// "+" — Tauri's add-to-playlist button, grouped into the shared
// "Add to…" flyout in 2.0.
QbzIconButton {
id: addBtn
name: "plus"
btnEnabled: QbzPlayer.npHasTrack
anchors.verticalCenter: parent.verticalCenter
onClicked: tc.addRequested(addBtn)
}
FavToggle {
visible: tc.classicActions
anchors.verticalCenter: parent.verticalCenter
}
}
// ============================ the bar =================================
@@ -440,12 +552,14 @@ Rectangle {
showBadges: !root.largeActive
}
// Classic: transport cluster hugging the left edge (plain
// play glyph, Tauri arrangement).
// play glyph + inline favorite, the Tauri arrangement).
TransportControls {
visible: root.isClassic
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
playCircle: false
classicActions: true
onAddRequested: function (anchorItem) { addMenu.openBelowRight(anchorItem) }
}
}
@@ -462,6 +576,7 @@ Rectangle {
visible: !root.isClassic
anchors.centerIn: parent
playCircle: true
onAddRequested: function (anchorItem) { addMenu.openBelowRight(anchorItem) }
}
// Classic: the contained glass song card (<=560px cap).
SongCard {
@@ -497,64 +612,90 @@ Rectangle {
}
Item { visible: root.largeActive; width: 12; height: 1 }
// Qobuz Connect — inert (device flyout out of scope).
QbzIconButton { name: "element-connect" }
// Now-Playing view — the mode flyout (phase 18).
// Cast (Chromecast / DLNA) — Tauri's first right-cluster
// button.
QbzIconButton {
name: "layout-grid"
active: viewMenu.opened
onClicked: viewMenu.openBelowRight(viewBtn)
id: viewBtn
name: "cast"
anchors.verticalCenter: parent.verticalCenter
// TODO(qt-bridge): no cast picker in the Qt port
// (Slint: CastState.picker-open + CastActions.open()).
// Rendered 1:1, inert.
}
// Qobuz Connect — inert (device flyout out of scope).
// monitor-speaker is the icon both Slint bars use.
QbzIconButton {
name: "monitor-speaker"
anchors.verticalCenter: parent.verticalCenter
// TODO(qt-bridge): no qconnect state/toggle exposed
// (Slint: NowPlayingState.qconnect-connected +
// qconnect-toggle()). Rendered 1:1, inert.
}
// Lyrics.
QbzIconButton {
name: "mic-vocal"
active: QbzShell.lyricsOpen
anchors.verticalCenter: parent.verticalCenter
onClicked: QbzShell.toggleLyrics()
}
// Now-Playing view — the mode flyout (phase 18). Tauri's
// Miniplayer + Full screen buttons live inside it.
QbzIconButton {
id: viewBtn
name: "layout-grid"
active: viewMenu.opened
anchors.verticalCenter: parent.verticalCenter
onClicked: viewMenu.openBelowRight(viewBtn)
}
// Audio settings — Tauri's normalization toggle (2.0
// groups normalization + gapless behind this button).
// TODO(qt-bridge): the Slint two-toggle flyout is not
// ported; the click toggles normalization directly, which
// is exactly what the Tauri button did.
QbzIconButton {
name: "settings-2"
active: root.normalizationOn
anchors.verticalCenter: parent.verticalCenter
onClicked: root.toggleNormalization()
}
Item { width: 6; height: 1 }
// Volume.
// Volume: mute · slider · −/+ steppers (the steppers are
// the Tauri volume-step buttons, gated by the appearance
// preference like the Slint bar).
QbzIconButton {
name: QbzPlayer.npMuted ? "volume-x" : "volume-2"
active: QbzPlayer.npMuted
anchors.verticalCenter: parent.verticalCenter
onClicked: QbzPlayer.toggleMute()
}
Item {
QbzSlider {
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 * QbzPlayer.npVolume
height: parent.height
radius: 2
color: theme.accent
}
Rectangle {
width: 16
height: 16
radius: 8
color: theme.textPrimary
x: parent.width * QbzPlayer.npVolume - width / 2
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onPressed: QbzPlayer.setVolume(Math.min(Math.max(mouseX / width, 0), 1))
onPositionChanged: if (pressed)
QbzPlayer.setVolume(Math.min(Math.max(mouseX / width, 0), 1))
}
anchors.verticalCenter: parent.verticalCenter
minimum: 0
// 0..1000: whole steps on a 0..100 scale quantize the
// volume to 1%; 0.1% steps drag fluidly.
maximum: 1000
value: Math.round(QbzPlayer.npVolume * 1000)
onChanged: function (v) { QbzPlayer.setVolume(v / 1000.0) }
}
QbzIconButton {
visible: root.showVolumeSteppers
name: "minus"
iconSize: 15
anchors.verticalCenter: parent.verticalCenter
onClicked: QbzPlayer.setVolume(Math.max(0.0, QbzPlayer.npVolume - 0.05))
}
QbzIconButton {
visible: root.showVolumeSteppers
name: "plus"
iconSize: 15
anchors.verticalCenter: parent.verticalCenter
onClicked: QbzPlayer.setVolume(Math.min(1.0, QbzPlayer.npVolume + 0.05))
}
Item { width: 6; height: 1 }
@@ -563,6 +704,7 @@ Rectangle {
QbzIconButton {
name: "list-ordered"
active: QbzShell.queueOpen
anchors.verticalCenter: parent.verticalCenter
onClicked: QbzShell.toggleQueue()
}
}
@@ -574,4 +716,50 @@ Rectangle {
// The Now-Playing-view mode menu (shared with the Small bar).
ViewModeMenu { id: viewMenu }
// "Add to…" flyout behind the transport "+" (TransportControls.slint's
// add-menu), on the shared CardMenu surface. Same seven entries, same
// order, same icons.
CardMenu {
id: addMenu
menuWidth: 232
entries: {
var m = [
{
"label": root.npFavorite
? QbzSession.tr("Remove from library", QbzSession.trRev)
: QbzSession.tr("Add to library", QbzSession.trRev),
"icon": root.npFavorite ? "heart-filled" : "heart",
"action": "favorite"
},
{ "label": QbzSession.tr("Add to playlist", QbzSession.trRev), "icon": "list-music", "action": "playlist" },
{ "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "action": "queue" },
{ "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "action": "later" },
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "action": "next" },
{ "label": QbzSession.tr("Add to mixtape", QbzSession.trRev), "icon": "cassette-tape", "action": "mixtape" },
]
if (QbzPlayer.npAlbumId !== "") {
m.push({
"label": QbzSession.tr("Add album to collection", QbzSession.trRev),
"icon": "disc-3",
"action": "album-favorite"
})
}
return m
}
onPicked: function (a) {
var id = QbzPlayer.npTrackId
if (a === "favorite") {
if (id !== "") QbzQueue.queueToggleFavorite("track", id)
} else if (a === "queue" || a === "later" || a === "next") {
if (id !== "") QbzPlayer.enqueueTrack(id, a)
} else if (a === "album-favorite") {
if (QbzPlayer.npAlbumId !== "")
QbzBridge.libraryToggleFavorite("album", QbzPlayer.npAlbumId)
}
// TODO(qt-bridge): "playlist" (add-to-playlist modal) and
// "mixtape" have no invokable in the Qt port yet — the rows are
// rendered 1:1 with the Slint flyout and do nothing for now.
}
}
}
+4
View File
@@ -571,6 +571,9 @@ pub struct SettingsDoc {
#[serde(rename = "showContextIcon")]
pub show_context_icon: bool,
pub gapless: bool,
/// Settable via settingsBool("normalization", …) but never published
/// back, so both now-playing bars had to shadow the toggle locally.
pub normalization: bool,
#[serde(rename = "persistSession")]
pub persist_session: bool,
#[serde(rename = "resumePosition")]
@@ -906,6 +909,7 @@ pub async fn publish_snapshot() {
continue_playback: prefs.autoplay_mode == AutoplayMode::ContinueWithinSource,
show_context_icon: prefs.show_context_icon,
gapless: audio_settings.gapless_enabled,
normalization: audio_settings.normalization_enabled,
persist_session: prefs.persist_session,
resume_position: prefs.resume_playback_position,
stream_uncached: audio_settings.stream_first_track,