Files
qbz/crates/qbz-qt/qml/controls/QbzSlider.qml
T
vicrodh b8b493bd59 fix(qt): persist the player volume, so a launch stops starting at 100%
PARITY-DEBT #3 — the cheapest AUDIBLE row in the inventory. Volume was never
written and never restored: `setVolume` reached the engine and stopped there.
Set it to 15%, quit, relaunch, and playback came back at full scale. On an
exclusive-mode DAC at night that is not a cosmetic default.

1:1 with the Slint: persist on drag-END only (`NowPlayingState.persist-volume`,
PlayerBar.slint:864-866 -> qbz/src/main.rs:14413-14417), restore at shell entry
(qbz/src/main.rs:220-227), and let the poll loop mirror the engine back onto the
slider so nothing else has to publish it.

Drag-end matters here rather than being a nicety: ui_prefs.json is co-owned with
the running Slint app and both processes do a whole-document read-modify-write,
so a write per drag tick is a write per pixel into a shared file. `QbzSlider`
only had `changed`, which fires on every tick, so it gains a `released` signal
carrying the settled value — both now-playing bars use it.

Two supporting bits: `QbzPlayer.persistVolume` (the engine setter stays
per-tick, only the persist is debounced by the signal), and
`settings_qt::read_pref_f32` — the crate had no public ui_prefs reader, theme_qt
kept a private one, and a third private copy is exactly what the write
discipline at the top of settings_qt exists to prevent.

Verified the key name against the live file: `volume` is already present in the
shared ui_prefs.json alongside 60 other keys, so this reads and writes what the
Slint build reads and writes.

cc
2026-07-30 09:46:19 -06:00

89 lines
3.1 KiB
QML

// QbzSlider (primitives/QbzSlider.slint) — extracted from SettingsView.qml
// in phase 19. 200x22, 4px r2 track, accent fill, 16px thumb; integer
// steps. Like the Slint original the thumb follows the pointer during a
// drag (local dragValue) and commits each step via changed(int).
import QtQuick
import com.blitzfc.qbz
import "../theme"
Item {
property int minimum: 0
property int maximum: 10
property int value: 0
signal changed(int newValue)
/// Fired ONCE on drag-end with the settled value. `changed` fires on every
/// drag tick, so anything that PERSISTS must listen here instead — the
/// volume slider writes ui_prefs.json, a document shared with the running
/// Slint app through a whole-file read-modify-write, and a write per pixel
/// of drag is exactly what `PlayerBar.slint:864-866` avoids ("Persist only
/// the final value on drag-end").
signal released(int newValue)
QbzTheme { id: theme }
width: 200
height: 22
// Disabled = inert (the bit-perfect ALSA direct path locks volume).
// `enabled` already disarms the MouseArea; this is the matching dim.
opacity: enabled ? 1.0 : 0.3
readonly property int thumbSize: 16
readonly property real travel: width - thumbSize
readonly property real fraction: maximum > minimum
? Math.max(0, Math.min(1, (value - minimum) / (maximum - minimum))) : 0
property bool dragging: false
property real dragFraction: fraction
readonly property real shownFraction: dragging ? dragFraction : fraction
onFractionChanged: if (!dragging) dragFraction = fraction
function commit(frac) {
const v = Math.round(minimum + Math.max(0, Math.min(1, frac)) * (maximum - minimum))
if (v !== value) changed(v)
}
Rectangle { // track
x: 0
y: Math.round((parent.height - height) / 2)
width: parent.width
height: 4
radius: 2
color: theme.surfaceElevated
}
Rectangle { // accent fill
x: 0
y: Math.round((parent.height - height) / 2)
width: parent.thumbSize / 2 + parent.shownFraction * parent.travel
height: 4
radius: 2
color: theme.accent
}
Rectangle { // thumb
width: parent.thumbSize
height: parent.thumbSize
radius: parent.thumbSize / 2
x: parent.shownFraction * parent.travel
anchors.verticalCenter: parent.verticalCenter
color: theme.textPrimary
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onPressed: {
parent.dragging = true
parent.dragFraction = Math.max(0, Math.min(1, (mouse.x - parent.thumbSize / 2) / parent.travel))
parent.commit(parent.dragFraction)
}
onPositionChanged: {
if (pressed) {
parent.dragFraction = Math.max(0, Math.min(1, (mouse.x - parent.thumbSize / 2) / parent.travel))
parent.commit(parent.dragFraction)
}
}
onReleased: {
parent.dragging = false
parent.released(parent.value)
}
}
}