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