// 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 { QbzIcon } from "../primitives/QbzIcon.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(); // Emitted by the refresh/release button next to the output device: // frees a device QBZ is holding (ALSA exclusive) and re-enumerates so a // freed or hot-plugged DAC appears without restarting the app. callback settings-release-device(); 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."); HorizontalLayout { spacing: 8px; alignment: center; // Refresh / release: frees a device QBZ is holding exclusively // (ALSA Direct) and re-enumerates, so a DAC that's invisible to // PipeWire — or one hot-plugged while QBZ ran — shows up without // an app restart. release-btn := Rectangle { width: 34px; height: 34px; border-radius: Radius.sm; border-width: 1px; border-color: Theme.border-subtle; background: release-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated; QbzIcon { width: 16px; height: 16px; source: @image-url("../assets/icons/refresh-cw.svg"); tint: release-ta.has-hover ? Theme.text-primary : Theme.text-muted; } release-ta := TouchArea { mouse-cursor: pointer; clicked => { root.settings-release-device(); } } } 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(); } } } } }