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
89 lines
3.1 KiB
QML
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)
|
|
}
|
|
}
|
|
}
|