Tauri parity (the sections did not survive the Slint port): a Settings section shown only on sandboxed installs holding the one-time permission commands with copy-to-clipboard buttons. Flatpak: exclusive-DAC D-Bus reservation override (the user-side fix for #641's exclusive-mode failure — the Flathub manifest cannot carry the wildcard own-name past its linter, so the user grants it locally) and NAS/external-drive filesystem access. Snap: the audio interface connections (qbz-player:alsa/pulseaudio/ pipewire/mpris) plus optional removable-media. New SandboxState global seeded from qbz_app::diagnostics install_method; 14 msgids across all 8 bundled locales. km
315 lines
13 KiB
Plaintext
315 lines
13 KiB
Plaintext
// Settings page — a back/title header, a left sub-navigation column
|
|
// (Audio, Playback), and the active settings panel in a scroll area.
|
|
//
|
|
// Navigation between sub-sections is pure UI state (SettingsState.section);
|
|
// setting changes bubble up as settings-bool / settings-select callbacks
|
|
// to the Rust layer, which persists them and applies them to the player.
|
|
|
|
import { Theme } from "../foundation/semantic-colors.slint";
|
|
import { Typography } from "../foundation/typography.slint";
|
|
import { Radius } from "../foundation/radius.slint";
|
|
import { NavState, ContentView, SettingsState, LogViewerState, SandboxState } from "../state.slint";
|
|
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
|
import { NavButtons } from "../shell/NavButtons.slint";
|
|
import { AudioSettings } from "AudioSettings.slint";
|
|
import { PlaybackSettings } from "PlaybackSettings.slint";
|
|
import { AppearanceSettings } from "AppearanceSettings.slint";
|
|
import { OfflineSettings } from "OfflineSettings.slint";
|
|
import { LocalLibrarySettings } from "LocalLibrarySettings.slint";
|
|
import { ContentFilteringSettings } from "ContentFilteringSettings.slint";
|
|
import { IntegrationsSettings } from "IntegrationsSettings.slint";
|
|
import { DeveloperSettings } from "DeveloperSettings.slint";
|
|
import { SandboxSettings } from "SandboxSettings.slint";
|
|
import { ScrollView } from "std-widgets.slint";
|
|
import { ListScrollbar } from "../primitives/ListScrollbar.slint";
|
|
|
|
component SubNavItem inherits Rectangle {
|
|
in property <image> icon;
|
|
in property <string> label;
|
|
in property <bool> active: false;
|
|
callback clicked();
|
|
|
|
height: 38px;
|
|
border-radius: Radius.sm;
|
|
background: root.active ? Theme.surface-elevated
|
|
: ta.has-hover ? Theme.surface-hover : transparent;
|
|
|
|
HorizontalLayout {
|
|
padding-left: 12px;
|
|
padding-right: 12px;
|
|
spacing: 10px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
QbzIcon {
|
|
source: root.icon;
|
|
width: 16px;
|
|
height: 16px;
|
|
tint: root.active ? Theme.text-primary : Theme.text-secondary;
|
|
}
|
|
}
|
|
Text {
|
|
text: root.label;
|
|
color: root.active ? Theme.text-primary : Theme.text-secondary;
|
|
font-size: Typography.body;
|
|
font-weight: root.active ? Typography.semibold : Typography.regular;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
}
|
|
}
|
|
|
|
ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.clicked();
|
|
}
|
|
}
|
|
}
|
|
|
|
export component SettingsView inherits Rectangle {
|
|
callback settings-bool(string, bool);
|
|
callback settings-select(string, int);
|
|
callback settings-slider(string, int);
|
|
callback settings-string(string, string);
|
|
callback settings-reset();
|
|
callback settings-release-device();
|
|
|
|
background: Theme.surface-main;
|
|
|
|
VerticalLayout {
|
|
spacing: 0px;
|
|
|
|
// --- Header: back button + title -----------------------------------
|
|
Rectangle {
|
|
height: 92px;
|
|
VerticalLayout {
|
|
padding-left: 32px;
|
|
padding-right: 32px;
|
|
padding-top: 11px;
|
|
spacing: 12px;
|
|
alignment: start;
|
|
|
|
HorizontalLayout {
|
|
alignment: start;
|
|
NavButtons { }
|
|
}
|
|
|
|
Text {
|
|
text: @tr("Settings");
|
|
color: Theme.text-primary;
|
|
font-size: Typography.title;
|
|
font-weight: Typography.bold;
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Sub-nav + active panel ----------------------------------------
|
|
HorizontalLayout {
|
|
vertical-stretch: 1;
|
|
|
|
// Left sub-navigation.
|
|
Rectangle {
|
|
width: 232px;
|
|
VerticalLayout {
|
|
padding-left: 24px;
|
|
padding-right: 12px;
|
|
padding-top: 4px;
|
|
padding-bottom: 16px;
|
|
spacing: 4px;
|
|
SubNavItem {
|
|
icon: @image-url("../assets/icons/volume-2.svg");
|
|
label: @tr("Audio");
|
|
active: SettingsState.section == 0;
|
|
clicked => {
|
|
SettingsState.section = 0;
|
|
}
|
|
}
|
|
SubNavItem {
|
|
icon: @image-url("../assets/icons/play.svg");
|
|
label: @tr("Playback");
|
|
active: SettingsState.section == 1;
|
|
clicked => {
|
|
SettingsState.section = 1;
|
|
}
|
|
}
|
|
SubNavItem {
|
|
icon: @image-url("../assets/icons/layers.svg");
|
|
label: @tr("Appearance");
|
|
active: SettingsState.section == 2;
|
|
clicked => {
|
|
SettingsState.section = 2;
|
|
}
|
|
}
|
|
SubNavItem {
|
|
icon: @image-url("../assets/icons/cloud-download.svg");
|
|
label: @tr("Offline");
|
|
active: SettingsState.section == 3;
|
|
clicked => {
|
|
SettingsState.section = 3;
|
|
}
|
|
}
|
|
SubNavItem {
|
|
icon: @image-url("../assets/icons/hard-drive.svg");
|
|
label: @tr("Local Library");
|
|
active: SettingsState.section == 4;
|
|
clicked => {
|
|
SettingsState.section = 4;
|
|
}
|
|
}
|
|
// Content filtering (Artist Blacklist) — placed between
|
|
// downloads (Offline) and Integrations per the Tauri order.
|
|
SubNavItem {
|
|
icon: @image-url("../assets/icons/blind-eye.svg");
|
|
label: @tr("Blacklist");
|
|
active: SettingsState.section == 7;
|
|
clicked => {
|
|
SettingsState.section = 7;
|
|
}
|
|
}
|
|
SubNavItem {
|
|
icon: @image-url("../assets/icons/refresh-cw.svg");
|
|
label: @tr("Integrations");
|
|
active: SettingsState.section == 6;
|
|
clicked => {
|
|
SettingsState.section = 6;
|
|
}
|
|
}
|
|
SubNavItem {
|
|
icon: @image-url("../assets/icons/bug.svg");
|
|
label: @tr("Developer");
|
|
active: SettingsState.section == 5;
|
|
clicked => {
|
|
SettingsState.section = 5;
|
|
}
|
|
}
|
|
// Sandbox (Flatpak/Snap) — only on sandboxed installs;
|
|
// holds the one-time permission commands (Tauri parity).
|
|
if SandboxState.install-method == "flatpak": SubNavItem {
|
|
icon: @image-url("../assets/icons/lock.svg");
|
|
label: @tr("Flatpak");
|
|
active: SettingsState.section == 8;
|
|
clicked => {
|
|
SettingsState.section = 8;
|
|
}
|
|
}
|
|
if SandboxState.install-method == "snap": SubNavItem {
|
|
icon: @image-url("../assets/icons/lock.svg");
|
|
label: @tr("Snap");
|
|
active: SettingsState.section == 8;
|
|
clicked => {
|
|
SettingsState.section = 8;
|
|
}
|
|
}
|
|
// Absorbs the leftover height so the log affordance below
|
|
// pins to the bottom of the sub-nav column.
|
|
Rectangle { }
|
|
// Always-visible log uploader entry (any Settings section):
|
|
// people filing issues could not find the uploader buried
|
|
// under Developer > Open log viewer, so logs never arrived
|
|
// with bug reports. Opens the same viewer (copy + upload).
|
|
SubNavItem {
|
|
icon: @image-url("../assets/icons/cloud-upload.svg");
|
|
label: @tr("Share logs");
|
|
clicked => {
|
|
LogViewerState.open = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Active panel. A raw Flickable (NOT the std ScrollView) so the
|
|
// panel scrolls on TOUCH-DRAG on the Pi kiosk — a Flickable intercepts
|
|
// the drag after a few px even over the dense SettingRows' controls,
|
|
// which the std ScrollView did not. A ListScrollbar overlay keeps the
|
|
// desktop scrollbar; sr-restore uses `self.height` (the Flickable's
|
|
// visible height) in place of ScrollView's `visible-height`.
|
|
Rectangle {
|
|
horizontal-stretch: 1;
|
|
settings-flick := Flickable {
|
|
interactive: true;
|
|
viewport-width: self.width;
|
|
property <string> sr-armed: NavState.restore-scope;
|
|
function sr-restore() {
|
|
if (NavState.restore-scope == "settings" && self.viewport-height > self.height) {
|
|
self.viewport-y = NavState.scroll-restore;
|
|
NavState.restore-scope = "";
|
|
}
|
|
}
|
|
init => { self.sr-restore(); }
|
|
changed sr-armed => { self.sr-restore(); }
|
|
changed viewport-height => { self.sr-restore(); }
|
|
changed viewport-y => { NavState.report-scroll(self.viewport-y); }
|
|
VerticalLayout {
|
|
padding-left: 20px;
|
|
padding-right: 40px;
|
|
padding-top: 4px;
|
|
padding-bottom: 60px;
|
|
alignment: start;
|
|
|
|
if SettingsState.loading: Text {
|
|
text: @tr("Loading settings...");
|
|
color: Theme.text-muted;
|
|
font-size: Typography.body;
|
|
}
|
|
|
|
if !SettingsState.loading && SettingsState.section == 0: AudioSettings {
|
|
settings-bool(k, v) => {
|
|
root.settings-bool(k, v);
|
|
}
|
|
settings-select(k, i) => {
|
|
root.settings-select(k, i);
|
|
}
|
|
settings-reset => {
|
|
root.settings-reset();
|
|
}
|
|
settings-release-device => {
|
|
root.settings-release-device();
|
|
}
|
|
}
|
|
if !SettingsState.loading && SettingsState.section == 1: PlaybackSettings {
|
|
settings-bool(k, v) => {
|
|
root.settings-bool(k, v);
|
|
}
|
|
settings-select(k, i) => {
|
|
root.settings-select(k, i);
|
|
}
|
|
settings-slider(k, v) => {
|
|
root.settings-slider(k, v);
|
|
}
|
|
settings-string(k, s) => {
|
|
root.settings-string(k, s);
|
|
}
|
|
}
|
|
// Appearance — a structural replica; rows bind to
|
|
// AppearanceState and emit its own callbacks, so no
|
|
// SettingsView-level callback forwarding is needed here.
|
|
if !SettingsState.loading && SettingsState.section == 2: AppearanceSettings { }
|
|
if !SettingsState.loading && SettingsState.section == 3: OfflineSettings {
|
|
settings-bool(k, v) => {
|
|
root.settings-bool(k, v);
|
|
}
|
|
}
|
|
if !SettingsState.loading && SettingsState.section == 4: LocalLibrarySettings { }
|
|
if !SettingsState.loading && SettingsState.section == 7: ContentFilteringSettings { }
|
|
if !SettingsState.loading && SettingsState.section == 6: IntegrationsSettings {
|
|
settings-bool(k, v) => {
|
|
root.settings-bool(k, v);
|
|
}
|
|
}
|
|
if !SettingsState.loading && SettingsState.section == 5: DeveloperSettings { }
|
|
if !SettingsState.loading && SettingsState.section == 8: SandboxSettings { }
|
|
}
|
|
}
|
|
ListScrollbar {
|
|
x: parent.width - self.width - 2px;
|
|
width: 14px;
|
|
height: 100%;
|
|
viewport-height: settings-flick.viewport-height;
|
|
visible-height: settings-flick.height;
|
|
viewport-y <=> settings-flick.viewport-y;
|
|
auto-hide: true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|