Files
qbz/crates/qbz-slint/ui/settings/SettingsView.slint
T
vicrodh 3826298f77 feat(locallibrary): Settings > Local Library — folder management (Slice A)
Port the Tauri folder-management surface to a new Settings > Local Library
sub-section (index 4); the browse-view gear routes here and pre-selects it.

UI:
- LocalLibrarySettings.slint: folder list (icon, display name, disabled/
  unavailable badges, multi-select checkboxes) + toolbar (scan-all/add/edit/
  remove icon buttons) + filter + Maintenance (cleanup missing) + two-step
  Danger Zone clear. ADR-008: plain bordered icon buttons + square badges.
- LibFolderEditModal.slint: alias, enabled, network override + fs-type,
  change-path, accessibility status, last-scanned, scan-this-folder. Mounted
  at the AppShell root (overlays the window, ADR-009 by mount order).
- SettingsView gains the section-4 sub-nav + panel; app.slint re-exports the
  new globals so the Rust layer can populate them.

Controller (local_library_settings.rs): load/add/remove/edit/save/change-path,
cleanup-missing (stats outside the DB lock), two-step clear, filter, select.
All DB access via qbz-library through library_db::with_db on spawn_blocking;
native dir picker + confirms via rfd. Selection preserved across reloads;
network folders get an async 6s-timeout accessibility check (exists() fallback
on timeout, mirroring Tauri).

Core (qbz-library): network_fs_label() for fs-type parity; separator-safe
delete_tracks_in_folder_prefixed() + remove_folder_with_tracks() (fixes the
prefix-collision bug without changing the legacy fn). Scan engine lands in
Slice B.

cc
2026-05-29 13:07:19 -06:00

204 lines
7.5 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 } 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 { ScrollView } from "std-widgets.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-reset();
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;
spacing: 4px;
alignment: start;
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;
}
}
}
}
// Active panel.
ScrollView {
horizontal-stretch: 1;
horizontal-scrollbar-policy: ScrollBarPolicy.always-off;
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();
}
}
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);
}
}
// 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 { }
if !SettingsState.loading && SettingsState.section == 4: LocalLibrarySettings { }
}
}
}
}
}