// 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 icon; in property label; in property 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 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; } } } } }