The six local SecondaryButton/ManageButton copies in the settings panels and the two inline AudioSettings buttons had fixed widths sized for English; Spanish/French/German labels overflowed the borders. Width is now Math.max(label preferred-width + padding, old floor), so short labels keep the 160/170/200px right-edge grid and long translations grow leftward instead of overflowing. cc
356 lines
14 KiB
Plaintext
356 lines
14 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, DacWizardActions } from "../state.slint";
|
|
import { QbzToggle } from "../primitives/QbzToggle.slint";
|
|
import { QbzSelect } from "../primitives/QbzSelect.slint";
|
|
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
|
import { WarningBanner } from "../primitives/WarningBanner.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);
|
|
}
|
|
}
|
|
}
|
|
// JACK routes through the JACK graph (resamples to the graph rate), so it
|
|
// can't be bit-perfect — surface that so the user isn't misled.
|
|
if SettingsState.backend-is-jack: WarningBanner {
|
|
variant: "warning";
|
|
title: @tr("JACK is not bit-perfect");
|
|
body: @tr("The JACK backend routes audio through the JACK graph, which resamples to the graph's sample rate. For bit-perfect playback use ALSA (direct) or PipeWire with passthrough.");
|
|
}
|
|
SettingRow {
|
|
label: @tr("Output device");
|
|
description: @tr("The DAC or sound device that receives audio.");
|
|
HorizontalLayout {
|
|
spacing: 8px;
|
|
// Right-align the refresh + select so the row's trailing controls
|
|
// land flush on the same right edge as the single-control rows when
|
|
// the window widens out of responsive mode (was `center`, which left
|
|
// a floating right-hand gap).
|
|
alignment: end;
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// HiFi Wizard — guided bit-perfect DAC setup (PipeWire only). Lives at the
|
|
// end of BIT-PERFECT: the whole point of the wizard is bit-perfect.
|
|
if SettingsState.backend-is-pipewire: SettingRow {
|
|
label: @tr("HiFi Wizard");
|
|
description: @tr("Auto-detect your DACs and set up bit-perfect playback, step by step.");
|
|
wizard-btn := Rectangle {
|
|
// 160px floor keeps the settings right-edge grid; longer translations grow.
|
|
width: Math.max(wizard-content.preferred-width + 32px, 160px);
|
|
height: 34px;
|
|
border-radius: Radius.sm;
|
|
border-width: 1px;
|
|
border-color: Theme.border-subtle;
|
|
background: wizard-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
|
|
wizard-content := HorizontalLayout {
|
|
alignment: center;
|
|
spacing: 8px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
QbzIcon {
|
|
width: 18px;
|
|
height: 18px;
|
|
source: @image-url("../assets/icons/gandalf.svg");
|
|
tint: Theme.text-primary;
|
|
}
|
|
}
|
|
Text {
|
|
text: @tr("Open Wizard");
|
|
color: Theme.text-primary;
|
|
font-size: Typography.body;
|
|
font-weight: Typography.medium;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
wizard-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
DacWizardActions.open();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
// 200px floor; longer translations grow instead of overflowing.
|
|
width: Math.max(reset-label.preferred-width + 32px, 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;
|
|
reset-label := 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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|