Files
qbz/crates/qbz-slint/ui/settings/AudioSettings.slint
T
vicrodh bdb9cadad6 feat(ui): speaker glyph + searchable output-device select
QbzSelect badges now render a "BP" tag for bit-perfect outputs and a
speaker glyph (volume-2) for the rest (real outputs that play audio but
not bit-perfect) — an addition, not a replacement. Only the device select
passes badges, so other selects are unchanged.

Add opt-in `searchable`: long output lists overflowed at 1440p, so the
open list gets a case-insensitive filter box and a 360px scroll cap.
Filtering hides non-matching rows by visibility only (model order intact,
so selected(i) keeps the original index). Backed by a new Rust
TextUtil.contains-ci pure callback (Slint 1.16 has no contains builtin).

cc
2026-05-31 16:50:29 -06:00

268 lines
9.3 KiB
Plaintext

// Audio settings panel — streaming quality, output routing, and
// bit-perfect options. 1:1 with the Tauri app. All rows are backed by
// the SettingsState global and persisted through the Rust settings
// controller. Conditional rows are `if`-gated on Rust-computed flags.
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { SettingsState } from "../state.slint";
import { QbzToggle } from "../primitives/QbzToggle.slint";
import { QbzSelect } from "../primitives/QbzSelect.slint";
import { SettingRow } from "SettingRow.slint";
component GroupHeader inherits Text {
color: Theme.text-muted;
font-size: 11px;
letter-spacing: 1.5px;
font-weight: Typography.semibold;
}
component Divider inherits Rectangle {
height: 1px;
background: Theme.border-subtle;
}
export component AudioSettings inherits VerticalLayout {
callback settings-bool(string, bool);
callback settings-select(string, int);
// Emitted by the Reset button.
callback settings-reset();
spacing: 4px;
GroupHeader { text: @tr("STREAMING"); }
SettingRow {
label: @tr("Streaming quality");
description: @tr("The quality tier QBZ requests for playback.");
QbzSelect {
menu-width: 200px;
options: SettingsState.streaming-qualities;
current-index: SettingsState.streaming-quality-index;
selected(i) => {
SettingsState.streaming-quality-index = i;
root.settings-select("streaming-quality", i);
}
}
}
SettingRow {
label: @tr("Limit quality to device");
description: @tr("Cap streaming quality at the output device's maximum sample rate.");
QbzToggle {
checked: SettingsState.limit-quality-to-device;
toggled(v) => {
SettingsState.limit-quality-to-device = v;
root.settings-bool("limit-quality-to-device", v);
}
}
}
if SettingsState.limit-quality-to-device: SettingRow {
label: @tr("Maximum sample rate");
description: @tr("The highest sample rate streamed tracks may use.");
QbzSelect {
menu-width: 170px;
options: SettingsState.sample-rates;
current-index: SettingsState.sample-rate-index;
selected(i) => {
SettingsState.sample-rate-index = i;
root.settings-select("sample-rate", i);
}
}
}
Rectangle { height: 12px; }
Divider { }
Rectangle { height: 12px; }
GroupHeader { text: @tr("OUTPUT"); }
SettingRow {
label: @tr("Audio backend");
description: @tr("The audio stack QBZ routes playback through.");
QbzSelect {
menu-width: 220px;
options: SettingsState.backends;
current-index: SettingsState.backend-index;
selected(i) => {
SettingsState.backend-index = i;
root.settings-select("backend", i);
}
}
}
SettingRow {
label: @tr("Output device");
description: @tr("The DAC or sound device that receives audio.");
QbzSelect {
menu-width: 300px;
// Device labels run long; open the list wider so the full
// name (card + ALSA PCM id) is readable.
popup-width: 480px;
// Output lists can be long (multiple DACs / HDMI sinks); a
// filter box + capped, scrolling list keeps it usable.
searchable: true;
options: SettingsState.devices;
badges: SettingsState.device-bp;
groups: SettingsState.device-groups;
current-index: SettingsState.device-index;
selected(i) => {
SettingsState.device-index = i;
root.settings-select("device", i);
}
}
}
if SettingsState.backend-is-alsa: SettingRow {
label: @tr("ALSA plugin");
description: @tr("How ALSA opens the device — hw is bit-perfect, plughw converts.");
QbzSelect {
menu-width: 220px;
options: SettingsState.alsa-plugins;
current-index: SettingsState.alsa-plugin-index;
selected(i) => {
SettingsState.alsa-plugin-index = i;
root.settings-select("alsa-plugin", i);
}
}
}
if SettingsState.backend-is-alsa && SettingsState.alsa-plugin-is-hw: SettingRow {
label: @tr("Hardware volume control");
description: @tr("Use the ALSA mixer for volume instead of software gain.");
QbzToggle {
checked: SettingsState.alsa-hardware-volume;
toggled(v) => {
SettingsState.alsa-hardware-volume = v;
root.settings-bool("alsa-hardware-volume", v);
}
}
}
Rectangle { height: 12px; }
Divider { }
Rectangle { height: 12px; }
GroupHeader { text: @tr("BIT-PERFECT"); }
SettingRow {
label: @tr("Exclusive mode");
description: @tr("Lock the device so no other app can resample it.");
// ALSA-only — disabled for PipeWire / Pulse / System default.
enabled: SettingsState.backend-is-alsa;
QbzToggle {
checked: SettingsState.exclusive-mode;
enabled: SettingsState.backend-is-alsa;
toggled(v) => {
SettingsState.exclusive-mode = v;
root.settings-bool("exclusive-mode", v);
}
}
}
SettingRow {
label: @tr("Reserve DAC while running");
description: @tr("Hold the device reserved so other apps can't grab it.");
QbzToggle {
checked: SettingsState.reserve-dac;
toggled(v) => {
SettingsState.reserve-dac = v;
root.settings-bool("reserve-dac", v);
}
}
}
SettingRow {
label: @tr("DAC passthrough");
description: @tr("Send the bitstream untouched to the DAC.");
// PipeWire-only — disabled for ALSA / Pulse / System default.
enabled: SettingsState.backend-is-pipewire;
QbzToggle {
checked: SettingsState.dac-passthrough;
enabled: SettingsState.backend-is-pipewire;
toggled(v) => {
SettingsState.dac-passthrough = v;
root.settings-bool("dac-passthrough", v);
}
}
}
if SettingsState.dac-passthrough && SettingsState.backend-is-pipewire: SettingRow {
label: @tr("Force bit-perfect");
description: @tr("Pin the PipeWire quantum and rate for the active track.");
QbzToggle {
checked: SettingsState.pw-force-bitperfect;
toggled(v) => {
SettingsState.pw-force-bitperfect = v;
root.settings-bool("pw-force-bitperfect", v);
}
}
}
SettingRow {
label: @tr("Allow quality fallback");
description: @tr("Drop to a lower tier when the requested one is unavailable.");
QbzToggle {
checked: SettingsState.allow-quality-fallback;
toggled(v) => {
SettingsState.allow-quality-fallback = v;
root.settings-bool("allow-quality-fallback", v);
}
}
}
Rectangle { height: 12px; }
Divider { }
Rectangle { height: 12px; }
GroupHeader { text: @tr("STARTUP"); }
SettingRow {
label: @tr("Sync audio settings on startup");
description: @tr("Reload saved audio settings into the player when QBZ launches.");
QbzToggle {
checked: SettingsState.sync-audio-on-startup;
toggled(v) => {
SettingsState.sync-audio-on-startup = v;
root.settings-bool("sync-audio-on-startup", v);
}
}
}
if SettingsState.backend-is-pipewire: SettingRow {
label: @tr("Lock output device");
description: @tr("Keep external routing intact — skip switching the default sink.");
// Mutually exclusive with DAC passthrough — disabled while it is on.
enabled: !SettingsState.dac-passthrough;
QbzToggle {
checked: SettingsState.skip-sink-switch;
enabled: !SettingsState.dac-passthrough;
toggled(v) => {
SettingsState.skip-sink-switch = v;
root.settings-bool("skip-sink-switch", v);
}
}
}
Rectangle { height: 20px; }
// Reset — restores Audio + Playback defaults.
HorizontalLayout {
alignment: start;
reset-btn := Rectangle {
width: 200px;
height: 36px;
border-radius: Radius.sm;
border-width: 1px;
border-color: Theme.border-subtle;
background: reset-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
Text {
text: @tr("Reset to defaults");
color: Theme.text-secondary;
font-size: Typography.body;
font-weight: Typography.medium;
horizontal-alignment: center;
vertical-alignment: center;
}
reset-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.settings-reset();
}
}
}
}
}