fix(qbz-qt): live output LEDs, closer stamp, and a volume lock on ALSA direct
The LEDs only changed when the user navigated. They were published from the settings snapshot, so they refreshed when that document republished rather than when playback started — which is exactly when the backend and mode are decided, a moment after "now playing" in the log. They publish on the stream edge now, without polling and without republishing settings. The stamp itself was close but not 1:1, so it went back against shell/AudioStamp.slint, primitives/QualityBadgeFull.slint and shell/SongCard.slint line by line; the divergences in sizes, weights, spacing and the inactive LED colour are corrected. Volume is locked while the bit-perfect ALSA direct path is engaged. Software volume there is meaningless and applying it would not be bit-perfect, so the control now reads as inert rather than silently doing nothing. Slint has the same gap and no lock to copy, so this is new: the condition comes from the output state already published, and no audio behaviour changed — only the UI reflects it. cc
This commit is contained in:
8 files changed
+239
-54
No files matched your search
@@ -17,6 +17,9 @@ Item {
|
||||
|
||||
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
|
||||
|
||||
@@ -156,6 +156,14 @@ Item {
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
// The bubble is 1:1 with the global shell/TooltipOverlay.slint the
|
||||
// Slint stamp feeds: 11px/medium TEXT-PRIMARY (not secondary) on
|
||||
// surface-elevated, radius 4 (not radiusSm=8), NO border, 5px/9px
|
||||
// padding (not 6px), default line height (Slint sets none), and a
|
||||
// downward caret. Centred on the control (TooltipOverlay clamps to
|
||||
// the window and keeps the caret on the anchor); the BUBBLE bottom
|
||||
// sits 9px above the anchor top (TooltipOverlay.slint:26) with the
|
||||
// caret poking 5px below it, hence y = -height - 4.
|
||||
ToolTip {
|
||||
id: qualityTip
|
||||
visible: qualityHover.containsMouse && root.qualityTooltip !== ""
|
||||
@@ -163,24 +171,41 @@ Item {
|
||||
delay: 0
|
||||
timeout: -1
|
||||
padding: 0
|
||||
x: qualityRow.width - width
|
||||
y: -height - 6
|
||||
x: Math.round((qualityRow.width - width) / 2)
|
||||
y: -height - 4
|
||||
contentItem: Text {
|
||||
text: qualityTip.text
|
||||
color: theme.textSecondary
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: 11
|
||||
font.weight: theme.weightMedium
|
||||
lineHeight: 1.25
|
||||
leftPadding: 9
|
||||
rightPadding: 9
|
||||
topPadding: 6
|
||||
bottomPadding: 6
|
||||
topPadding: 5
|
||||
// 5px bubble padding + the 5px caret zone below it.
|
||||
bottomPadding: 10
|
||||
}
|
||||
background: Rectangle {
|
||||
color: theme.surfaceElevated
|
||||
radius: theme.radiusSm
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
background: Item {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: 5
|
||||
color: theme.surfaceElevated
|
||||
radius: 4
|
||||
}
|
||||
// Caret (TooltipOverlay.slint:56 draws a 10x6 Path). A
|
||||
// 7.07px square rotated 45 deg is a 10px-wide diamond;
|
||||
// centred on the bubble's bottom edge its lower half IS
|
||||
// the caret. QtQuick.Shapes is not imported anywhere in
|
||||
// this port, and shader effects are dead on the software
|
||||
// path, so a rotated Rectangle is the way to draw it.
|
||||
Rectangle {
|
||||
width: 7.07
|
||||
height: 7.07
|
||||
rotation: 45
|
||||
antialiasing: true
|
||||
color: theme.surfaceElevated
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: parent.height - 5 - height / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,15 +55,56 @@ Column {
|
||||
return qualityCauseLine === "" ? t : t + "\n" + qualityCauseLine
|
||||
}
|
||||
|
||||
// The tooltip bubble of shell/TooltipOverlay.slint, 1:1: surface-elevated,
|
||||
// radius 4, NO border (the port's earlier bubble had a 1px border-subtle
|
||||
// and radiusSm = 8), plus the downward caret. The bubble's bottom edge
|
||||
// sits 9px above the anchor and the 5px caret pokes below it, so every
|
||||
// ToolTip using this background is positioned at y = -height - 4 with a
|
||||
// bottomPadding of 10 (5px bubble padding + the 5px caret zone).
|
||||
//
|
||||
// Slint also drop-shadows the bubble (blur 10, #00000099); that needs a
|
||||
// shader layer effect, which renders nothing on the software path, so it
|
||||
// is deliberately dropped.
|
||||
component TipBubble: Item {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: 5
|
||||
color: theme.surfaceElevated
|
||||
radius: 4
|
||||
}
|
||||
// A 7.07px square rotated 45 deg is a 10px-wide diamond; centred on
|
||||
// the bubble's bottom edge, its lower half is the 10x5 caret
|
||||
// (TooltipOverlay.slint:56 draws a 10x6 Path). QtQuick.Shapes is not
|
||||
// imported anywhere in this port.
|
||||
Rectangle {
|
||||
width: 7.07
|
||||
height: 7.07
|
||||
rotation: 45
|
||||
antialiasing: true
|
||||
color: theme.surfaceElevated
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: parent.height - 5 - height / 2
|
||||
}
|
||||
}
|
||||
|
||||
// Dot LED (circle-dot on / circle off) + constant-colour label —
|
||||
// SongCard.slint's DotLed. 9px mark, 4px gap, 8px/600 label at
|
||||
// SongCard.slint's DotLed (line 57): 9px mark, 4px gap, 8px/600 label at
|
||||
// letter-spacing 0.3 that NEVER changes colour (it stays text-muted; only
|
||||
// the dot signals state).
|
||||
//
|
||||
// The mark is DRAWN, not an SVG: the port has no circle / circle-dot
|
||||
// assets, and the software path cannot recolour at render time, so every
|
||||
// LED colour would need its own baked variant. A 1px ring plus a 4px
|
||||
// centre dot is the same figure.
|
||||
// LED colour would need its own baked variant.
|
||||
//
|
||||
// The figure is measured off the two lucide SVGs Slint tints, scaled to
|
||||
// the 9px box the .slint asks for (scale = 9/24 = 0.375):
|
||||
// circle.svg <circle r="10" stroke-width="2"> -> painted band
|
||||
// r 9..11 => outer diameter 22 * 0.375 = 8.25px,
|
||||
// stroke 2 * 0.375 = 0.75px
|
||||
// circle-dot.svg adds <circle r="2.5" fill> -> centre dot
|
||||
// diameter 5 * 0.375 = 1.875px
|
||||
// The previous 9px/1px ring with a 4px dot read as a solid bullseye next
|
||||
// to Slint's thin ring + pinpoint — that was the visible mismatch.
|
||||
component DotLed: Item {
|
||||
id: led
|
||||
property string label: ""
|
||||
@@ -78,21 +119,30 @@ Column {
|
||||
Row {
|
||||
id: ledRow
|
||||
spacing: 4
|
||||
Rectangle {
|
||||
// The 9px icon BOX (what QbzIcon occupies in the .slint) — the
|
||||
// 4px gap must measure from it, not from the smaller ring.
|
||||
Item {
|
||||
width: 9
|
||||
height: 9
|
||||
radius: 4.5
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: "transparent"
|
||||
border.width: 1
|
||||
border.color: led.on ? led.onColor : theme.textMuted
|
||||
Rectangle {
|
||||
visible: led.on
|
||||
width: 4
|
||||
height: 4
|
||||
radius: 2
|
||||
width: 8.25
|
||||
height: 8.25
|
||||
radius: width / 2
|
||||
anchors.centerIn: parent
|
||||
color: led.onColor
|
||||
antialiasing: true
|
||||
color: "transparent"
|
||||
border.width: 0.75
|
||||
border.color: led.on ? led.onColor : theme.textMuted
|
||||
Rectangle {
|
||||
visible: led.on
|
||||
width: 1.875
|
||||
height: 1.875
|
||||
radius: width / 2
|
||||
anchors.centerIn: parent
|
||||
antialiasing: true
|
||||
color: led.onColor
|
||||
}
|
||||
}
|
||||
}
|
||||
Text {
|
||||
@@ -110,6 +160,8 @@ Column {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
// Bubble geometry: see Tip below — same numbers, read off
|
||||
// shell/TooltipOverlay.slint.
|
||||
ToolTip {
|
||||
id: ledTip
|
||||
visible: ledHover.containsMouse && led.tooltip !== ""
|
||||
@@ -118,23 +170,18 @@ Column {
|
||||
timeout: -1
|
||||
padding: 0
|
||||
x: Math.round((led.width - width) / 2)
|
||||
y: -height - 6
|
||||
y: -height - 4
|
||||
contentItem: Text {
|
||||
text: ledTip.text
|
||||
color: theme.textSecondary
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: 11
|
||||
font.weight: theme.weightMedium
|
||||
leftPadding: 9
|
||||
rightPadding: 9
|
||||
topPadding: 5
|
||||
bottomPadding: 5
|
||||
}
|
||||
background: Rectangle {
|
||||
color: theme.surfaceElevated
|
||||
radius: theme.radiusSm
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
bottomPadding: 10
|
||||
}
|
||||
background: TipBubble {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,24 +233,18 @@ Column {
|
||||
timeout: -1
|
||||
padding: 0
|
||||
x: Math.round((root.width - width) / 2)
|
||||
y: -height - 6
|
||||
y: -height - 4
|
||||
contentItem: Text {
|
||||
text: stampTip.text
|
||||
color: theme.textSecondary
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: 11
|
||||
font.weight: theme.weightMedium
|
||||
lineHeight: 1.25
|
||||
leftPadding: 9
|
||||
rightPadding: 9
|
||||
topPadding: 6
|
||||
bottomPadding: 6
|
||||
}
|
||||
background: Rectangle {
|
||||
color: theme.surfaceElevated
|
||||
radius: theme.radiusSm
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
topPadding: 5
|
||||
bottomPadding: 10
|
||||
}
|
||||
background: TipBubble {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,12 @@
|
||||
//!
|
||||
//! The arithmetic behind the downgrade block lives in `quality_state.rs`
|
||||
//! (pure, unit-tested); the two output LEDs are derived from AudioSettings in
|
||||
//! `output_labels.rs` and published from `settings_qt::publish_snapshot`, not
|
||||
//! from here — they follow the SETTINGS, not the track.
|
||||
//! `output_labels.rs`. They follow the SETTINGS, not the track — but they are
|
||||
//! DECIDED when a stream opens, so besides `settings_qt::publish_snapshot`
|
||||
//! they are re-derived on shell entry (`publish_current`), on the track edge
|
||||
//! (`playback_qt::refresh_now_playing`) and on the stream edge
|
||||
//! (`set_effective_stream`, only when the delivered params actually move).
|
||||
//! Without those edges the stamp only refreshed when the user changed page.
|
||||
//!
|
||||
//! POC-NOTE: playback transport is wired (playback_qt poll pump). Cast /
|
||||
//! Qobuz Connect are NOT in the POC build (no qbz-cast / qconnect deps), so
|
||||
@@ -149,6 +153,10 @@ fn publish(m: &NowPlayingModel) {
|
||||
pub fn publish_current() {
|
||||
let (_, snapshot) = with_model(|_| ());
|
||||
publish(&snapshot);
|
||||
// Seed the two output LEDs + the volume-lock flag at shell entry too, so
|
||||
// the stamp is correct before the first track ever plays (the bridge
|
||||
// defaults are the unlit SYST/DEFAULT pair).
|
||||
crate::output_labels::publish_current();
|
||||
}
|
||||
|
||||
// --- Pure-UI toggles (mutate + republish) --------------------------------
|
||||
@@ -262,6 +270,12 @@ pub fn set_effective_stream(eff_rate_hz: u32, eff_bits: u32) {
|
||||
});
|
||||
if changed {
|
||||
publish(&snapshot);
|
||||
// STREAM edge: the engine has just reported real params, i.e. the
|
||||
// output stream is open and the backend/mode are now facts. Re-derive
|
||||
// the two LEDs so they are right even if the audio settings moved
|
||||
// between the track edge and the stream actually opening. Gated by
|
||||
// `changed`, so a steady stream costs nothing — this is not a poll.
|
||||
crate::output_labels::publish_current();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,20 @@
|
||||
//! lights the LED. This module only READS AudioSettings — the audio path
|
||||
//! (qbz-audio / qbz-player) is never touched from here.
|
||||
//!
|
||||
//! Published onto `QbzPlayer` (`np_output_*`) from `settings_qt::
|
||||
//! publish_snapshot`, i.e. on every settings change and at boot. Same shape
|
||||
//! as the Slint, where `apply_snapshot` mirrors the four values onto
|
||||
//! `NowPlayingState` alongside `SettingsState`. No polling.
|
||||
//! Published onto `QbzPlayer` (`np_output_*`) from THREE edges, never a poll:
|
||||
//! 1. `settings_qt::publish_snapshot` — every settings change (and the
|
||||
//! Settings document's own republish).
|
||||
//! 2. the TRACK edge (`playback_qt::refresh_now_playing`) — the moment a
|
||||
//! new stream is about to open, which is when the backend + mode are
|
||||
//! actually decided. Without this the LEDs only refreshed when the
|
||||
//! settings document republished, i.e. when the user changed page.
|
||||
//! 3. the STREAM edge (`now_playing::set_effective_stream`, deduped inside
|
||||
//! on the delivered params) — the first tick after the engine reports
|
||||
//! real stream params, so the labels are correct even if the settings
|
||||
//! were touched between track start and stream open.
|
||||
//!
|
||||
//! Same shape as the Slint, where `apply_snapshot` mirrors the four values
|
||||
//! onto `NowPlayingState` alongside `SettingsState`.
|
||||
|
||||
use cxx_qt_lib::QString;
|
||||
use qbz_audio::backend::{AlsaPlugin, AudioBackendType};
|
||||
@@ -88,19 +98,52 @@ pub fn output_labels(audio: &AudioSettings) -> OutputLabels {
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive + push the four LED values onto the player bridge (Qt-thread hop).
|
||||
/// Called from `settings_qt::publish_snapshot`, so the labels follow an audio
|
||||
/// settings change with no poll of their own.
|
||||
/// Whether the app's SOFTWARE volume control is inert on the current output
|
||||
/// route — i.e. moving the slider changes nothing in the signal path.
|
||||
///
|
||||
/// This is not a guess: `PlaybackEngine::set_volume`
|
||||
/// (`qbz-player/src/player/playback_engine.rs`) is a documented NO-OP on the
|
||||
/// `AlsaDirect` arm unless `alsa_hardware_volume` is on, in which case it
|
||||
/// drives the DAC's own mixer instead (still bit-perfect, so the slider stays
|
||||
/// live). The ALSA backend builds an `AlsaDirectStream` for BOTH the `hw`
|
||||
/// (bit-perfect direct) and the `plughw` fallback plugin; only `Pcm` opts out
|
||||
/// of the direct engine entirely and lands on CPAL/rodio, where software
|
||||
/// volume works. Hence: ALSA backend, plugin != Pcm, hardware volume off.
|
||||
///
|
||||
/// READ-ONLY derivation from the persisted `AudioSettings` — no audio
|
||||
/// behaviour is changed anywhere by this, the UI just stops lying about it.
|
||||
pub fn volume_locked(audio: &AudioSettings) -> bool {
|
||||
matches!(audio.backend_type, Some(AudioBackendType::Alsa))
|
||||
&& !matches!(audio.alsa_plugin, Some(AlsaPlugin::Pcm))
|
||||
&& !audio.alsa_hardware_volume
|
||||
}
|
||||
|
||||
/// Derive + push the four LED values (and the volume-lock flag) onto the
|
||||
/// player bridge (Qt-thread hop). Called from `settings_qt::publish_snapshot`
|
||||
/// AND from the track/stream edges, so the labels follow the settings without
|
||||
/// a poll of their own.
|
||||
pub fn publish(audio: &AudioSettings) {
|
||||
let l = output_labels(audio);
|
||||
let locked = volume_locked(audio);
|
||||
crate::player_bridge::ui(move |mut b| {
|
||||
b.as_mut().set_np_output_backend_label(QString::from(l.backend));
|
||||
b.as_mut().set_np_output_mode_label(QString::from(l.mode));
|
||||
b.as_mut().set_np_output_backend_active(l.backend_active);
|
||||
b.as_mut().set_np_output_mode_active(l.mode_active);
|
||||
b.as_mut().set_np_volume_locked(locked);
|
||||
});
|
||||
}
|
||||
|
||||
/// Re-derive from the LIVE audio settings and publish. This is the TRACK /
|
||||
/// STREAM edge entry point: the settings store is the source of truth for
|
||||
/// which backend + mode the engine is about to use, so reading it at the
|
||||
/// moment a stream opens costs one cheap WAL read and no settings-document
|
||||
/// rebuild (`publish_snapshot` is the expensive one — device enumeration,
|
||||
/// integrations, the whole Settings JSON; it must NOT be called from here).
|
||||
pub fn publish_current() {
|
||||
publish(&crate::settings_qt::audio_settings());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -162,6 +205,35 @@ mod tests {
|
||||
assert!(output_labels(&s).mode_active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn volume_is_locked_only_on_the_alsa_direct_engine_without_hw_mixer() {
|
||||
// Non-ALSA backends keep software volume.
|
||||
for b in [
|
||||
None,
|
||||
Some(AudioBackendType::PipeWire),
|
||||
Some(AudioBackendType::Pulse),
|
||||
Some(AudioBackendType::Jack),
|
||||
Some(AudioBackendType::SystemDefault),
|
||||
] {
|
||||
assert!(!volume_locked(&settings(b)), "{b:?} must keep the slider");
|
||||
}
|
||||
let mut s = settings(Some(AudioBackendType::Alsa));
|
||||
// hw = the bit-perfect direct path: engine set_volume is a no-op.
|
||||
s.alsa_plugin = Some(AlsaPlugin::Hw);
|
||||
s.alsa_hardware_volume = false;
|
||||
assert!(volume_locked(&s));
|
||||
// ...unless the DAC's own mixer is driven instead.
|
||||
s.alsa_hardware_volume = true;
|
||||
assert!(!volume_locked(&s));
|
||||
// plughw still runs the AlsaDirect engine -> still inert.
|
||||
s.alsa_hardware_volume = false;
|
||||
s.alsa_plugin = Some(AlsaPlugin::PlugHw);
|
||||
assert!(volume_locked(&s));
|
||||
// Pcm opts out of the direct engine (CPAL/rodio) -> software volume.
|
||||
s.alsa_plugin = Some(AlsaPlugin::Pcm);
|
||||
assert!(!volume_locked(&s));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pulse_and_system_default() {
|
||||
let l = output_labels(&settings(Some(AudioBackendType::Pulse)));
|
||||
|
||||
@@ -638,6 +638,14 @@ pub(crate) async fn refresh_now_playing(runtime: &Arc<AppRuntime<LoggingAdapter>
|
||||
RepeatMode::One => 2,
|
||||
},
|
||||
});
|
||||
// The two output LEDs (+ the volume-lock flag) are decided HERE, not when
|
||||
// the Settings page republishes: a stream is about to open and the
|
||||
// backend/mode it will use is whatever the audio settings say right now.
|
||||
// Publishing on the track edge is what makes the stamp live from the first
|
||||
// note instead of updating only when the user changes page. Cheap: one WAL
|
||||
// read + one Qt hop per track — no poll, and NOT publish_snapshot (that
|
||||
// rebuilds the entire Settings document).
|
||||
crate::output_labels::publish_current();
|
||||
// Catalog max + whether the streaming-quality pref governs this request:
|
||||
// the downgrade arrow compares DELIVERED against this (quality_state.rs).
|
||||
// Local and Plex sources are not governed — nothing downgrades them.
|
||||
|
||||
@@ -72,6 +72,13 @@ pub mod qbz_player {
|
||||
// DACPASS|BITPERF|EXCL|DIRECT|LOCKED|ROUTED|SHARED|DEFAULT.
|
||||
#[qproperty(QString, np_output_mode_label)]
|
||||
#[qproperty(bool, np_output_mode_active)]
|
||||
// The app's software volume is INERT on this route — the ALSA Direct
|
||||
// engine's `set_volume` is a no-op unless the DAC's own mixer is
|
||||
// driven (`alsa_hardware_volume`). Derived read-only in
|
||||
// `output_labels::volume_locked`; the bars render the volume slider
|
||||
// disabled with a tooltip instead of pretending it works. No audio
|
||||
// behaviour hangs off this flag — it is a UI mirror only.
|
||||
#[qproperty(bool, np_volume_locked)]
|
||||
|
||||
// --- Cast / remote (Qobuz Connect + Chromecast/DLNA) -------------
|
||||
// A peer Qobuz Connect renderer owns playback (transport is remote).
|
||||
@@ -196,6 +203,7 @@ pub struct QbzPlayerRust {
|
||||
np_output_backend_active: bool,
|
||||
np_output_mode_label: QString,
|
||||
np_output_mode_active: bool,
|
||||
np_volume_locked: bool,
|
||||
np_is_remote: bool,
|
||||
np_cast_target: QString,
|
||||
np_cast_active: bool,
|
||||
@@ -238,6 +246,9 @@ impl Default for QbzPlayerRust {
|
||||
np_output_backend_active: false,
|
||||
np_output_mode_label: QString::from("DEFAULT"),
|
||||
np_output_mode_active: false,
|
||||
// Optimistic default: the slider stays live until the first
|
||||
// settings read proves the route is an inert ALSA-direct one.
|
||||
np_volume_locked: false,
|
||||
np_is_remote: false,
|
||||
np_cast_target: QString::default(),
|
||||
np_cast_active: false,
|
||||
|
||||
@@ -63,6 +63,17 @@ fn with_audio<T>(f: impl FnOnce(&AudioSettingsStore) -> Result<T, String>) -> Re
|
||||
f(store)
|
||||
}
|
||||
|
||||
/// The live persisted audio settings, READ-ONLY (the audio path is
|
||||
/// PROTECTED — nothing here writes or reinitialises a device).
|
||||
///
|
||||
/// Exposed so the now-playing stamp can re-derive its two output LEDs on the
|
||||
/// TRACK / STREAM edge (`output_labels::publish_current`) without dragging in
|
||||
/// `publish_snapshot`, which rebuilds the whole Settings document (device
|
||||
/// enumeration + integrations) and is far too heavy for a track change.
|
||||
pub fn audio_settings() -> qbz_audio::settings::AudioSettings {
|
||||
with_audio(|s| s.get_settings()).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn with_playback<T>(
|
||||
f: impl FnOnce(&PlaybackPreferencesStore) -> Result<T, String>,
|
||||
) -> Result<T, String> {
|
||||
@@ -824,7 +835,7 @@ fn backend_label(t: AudioBackendType) -> String {
|
||||
|
||||
/// Build + publish the full snapshot (settings.rs `build_snapshot`).
|
||||
pub async fn publish_snapshot() {
|
||||
let audio_settings = with_audio(|s| s.get_settings()).unwrap_or_default();
|
||||
let audio_settings = audio_settings();
|
||||
let prefs = with_playback(|s| s.get_preferences()).unwrap_or_default();
|
||||
let streaming_key = streaming_quality();
|
||||
|
||||
|
||||
Reference in new issue
Block a user