Files
qbz/crates/qbz-ui/ui/settings/AppearanceSettings.slint
T
vicrodh 55caada4c4 feat(ui): play-indicator toggle, swipe inversion, accent-ring row mark
- Appearance > Play indicator animation (default OFF): static play glyph
  in an accent ring on the playing row, pause on hover, accent edge pill;
  the 8fps eq-bars clock no longer runs while playing with the pref off
  (CPU complaint on some machines).
- Appearance > Invert swipe navigation direction for the two-finger
  touchpad back/forward gesture (#653 follow-up).

km
2026-07-25 12:04:57 -06:00

1048 lines
44 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Appearance settings panel — theme, typography, language, window chrome,
// notifications, player views, system tray, and immersive options. A 1:1
// structural replica of the Tauri app's Settings > Appearance section.
//
// This is primarily an UNWIRED replica: rows are bound to AppearanceState
// and emit generic appearance-bool / appearance-select / appearance-text /
// appearance-action callbacks, but the Rust side does not yet persist or
// apply most of them. The single exception is the sidebar-playlist-collage
// toggle, which is wired straight through to SidebarState.playlist-collage
// (an already-functional flag). Conditional rows are `if`-gated on the same
// flags Tauri gates on (System theme, title-bar mode, etc.).
import { Theme } from "../foundation/semantic-colors.slint";
import { ThemeState } from "../foundation/theme-state.slint";
import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { AppearanceState, SidebarState, ShellState, ToastState, MyQbzBrandingState , UiFocusState } from "../state.slint";
import { QbzToggle } from "../primitives/QbzToggle.slint";
import { QbzSelect } from "../primitives/QbzSelect.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { ColorPicker } from "../primitives/ColorPicker.slint";
import { SettingRow } from "SettingRow.slint";
import { LineEdit } from "std-widgets.slint";
component GroupHeader inherits Text {
color: Theme.text-muted;
font-size: 11px;
letter-spacing: 1.5px;
font-weight: Typography.semibold;
}
// A small muted sub-group label inside the custom-theme editor grid.
component TokenGroupLabel inherits Text {
color: Theme.text-muted;
font-size: 10px;
letter-spacing: 1px;
font-weight: Typography.semibold;
}
// One editable base-token CELL in the custom-theme editor: a compact color
// swatch with its label underneath, laid INLINE with its group siblings.
// Clicking toggles the shared ColorPicker at the bottom of the editor panel
// (one picker for the whole grid keeps the layout stable while editing).
component CustomTokenCell inherits VerticalLayout {
in property <string> token-key;
in property <string> label;
in property <color> swatch-color;
spacing: 6px;
alignment: start;
property <bool> open: AppearanceState.custom-open-token == root.token-key;
Rectangle {
width: 56px;
height: 32px;
border-radius: Radius.sm;
border-width: root.open ? 2px : 1px;
border-color: root.open ? Theme.accent : Theme.border-subtle;
background: root.swatch-color;
TouchArea {
mouse-cursor: pointer;
clicked => {
AppearanceState.custom-open-token =
AppearanceState.custom-open-token == root.token-key ? "" : root.token-key;
}
}
}
Text {
text: root.label;
color: root.open ? Theme.text-primary : Theme.text-muted;
font-size: Typography.legal;
width: 56px;
horizontal-alignment: center;
overflow: elide;
}
}
component Divider inherits Rectangle {
height: 1px;
background: Theme.border-subtle;
}
// A small secondary-style button used for "Select Image…" / "Regenerate".
component SecondaryButton inherits Rectangle {
in property <string> label;
in property <bool> enabled: true;
callback clicked();
// 160px floor keeps the settings right-edge grid; longer translations grow.
width: Math.max(label-txt.preferred-width + 32px, 160px);
height: 34px;
border-radius: Radius.sm;
border-width: 1px;
border-color: Theme.border-subtle;
background: ta.has-hover && root.enabled ? Theme.surface-hover : Theme.surface-elevated;
opacity: root.enabled ? 1.0 : 0.4;
label-txt := Text {
text: root.label;
color: Theme.text-secondary;
font-size: Typography.body;
font-weight: Typography.medium;
horizontal-alignment: center;
vertical-alignment: center;
}
ta := TouchArea {
mouse-cursor: root.enabled ? pointer : default;
clicked => {
if (root.enabled) {
root.clicked();
}
}
}
}
// A collapsible sub-section header (Immersive Panels / Animation Frame Rate).
// Hidden: its only users were the IMMERSIVE block (now commented out). Kept
// commented for re-enable alongside that block; do NOT delete.
/*
component CollapsibleHeader inherits Rectangle {
in property <string> title;
in property <string> summary;
in property <bool> collapsed: true;
callback toggled();
height: 56px;
HorizontalLayout {
spacing: 8px;
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
spacing: 3px;
Text {
text: root.title;
color: Theme.text-primary;
font-size: Typography.body;
font-weight: Typography.semibold;
}
Text {
text: root.summary;
color: Theme.text-muted;
font-size: 12px;
}
}
VerticalLayout {
alignment: center;
QbzIcon {
source: root.collapsed
? @image-url("../assets/icons/chevron-down.svg")
: @image-url("../assets/icons/chevron-up.svg");
width: 16px;
height: 16px;
tint: Theme.text-secondary;
}
}
}
ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.toggled();
}
}
}
*/
export component AppearanceSettings inherits VerticalLayout {
spacing: 4px;
// Derived: title-bar customization rows are disabled when the title bar
// is hidden or the system title bar is used (mirrors Tauri).
property <bool> tb-locked: AppearanceState.hide-title-bar || AppearanceState.use-system-title-bar;
// ===================================================================
// THEME
// ===================================================================
GroupHeader { text: @tr("THEME"); }
SettingRow {
label: @tr("Theme");
HorizontalLayout {
spacing: 8px;
// Theme list filter cycle (All / Dark / Light).
filter-btn := Rectangle {
width: 34px;
height: 34px;
border-radius: Radius.sm;
border-width: 1px;
border-color: Theme.border-subtle;
background: filter-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
QbzIcon {
// 0 = All, 1 = Dark, 2 = Light (SunMoon / Moon / Sun).
source: AppearanceState.theme-filter == 1
? @image-url("../assets/icons/moon.svg")
: AppearanceState.theme-filter == 2
? @image-url("../assets/icons/sun.svg")
: @image-url("../assets/icons/sun-moon.svg");
width: 16px;
height: 16px;
tint: Theme.text-secondary;
}
filter-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
AppearanceState.theme-filter = Math.mod(AppearanceState.theme-filter + 1, 3);
AppearanceState.appearance-select("theme-filter", AppearanceState.theme-filter);
}
}
}
QbzSelect {
menu-width: 220px;
// 36 registered themes — a name filter (Tauri parity). The search
// box hides non-matching rows but preserves model order, so
// `selected(i)` stays the index into the (polarity-filtered)
// `themes` model that the Rust "theme" handler maps.
searchable: true;
options: AppearanceState.themes;
current-index: AppearanceState.theme-index;
selected(i) => {
AppearanceState.theme-index = i;
// The Rust handler ("theme" arm) maps this dropdown index to
// a stable theme id, persists the slug, and hot-switches the
// palette via Theme.set_c. ThemeState.mode is kept in sync
// only as a legacy index reference (no longer drives colors).
ThemeState.mode = i;
AppearanceState.appearance-select("theme", i);
}
}
}
}
SettingRow {
label: @tr("Album header gradient");
description: @tr("Use artwork-derived blur as a backdrop in album and artist detail views.");
QbzToggle {
checked: AppearanceState.album-header-gradient;
toggled(v) => {
AppearanceState.album-header-gradient = v;
AppearanceState.appearance-bool("album-header-gradient", v);
}
}
}
if AppearanceState.app-background-available: SettingRow {
label: @tr("Dynamic background");
description: @tr("Animated album-art background behind the whole app. High resource use — GPU accelerated.");
QbzSelect {
menu-width: 200px;
options: AppearanceState.app-background-modes;
current-index: AppearanceState.app-background-mode-index;
selected(i) => {
AppearanceState.app-background-mode-index = i;
AppearanceState.appearance-select("app-background", i);
}
}
}
SettingRow {
label: @tr("Intelligent Search");
description: @tr("Smart search cache, ranking, and the search preview dropdown. On by default.");
QbzToggle {
checked: AppearanceState.intelligent-search;
toggled(v) => {
AppearanceState.intelligent-search = v;
AppearanceState.appearance-bool("intelligent-search", v);
}
}
}
// Auto-Theme controls — only when the "Auto (dynamic)" theme is selected.
if AppearanceState.theme-is-auto: SettingRow {
label: @tr("Source");
description: @tr("Generate a color theme from your system wallpaper or a custom image");
QbzSelect {
menu-width: 200px;
options: AppearanceState.auto-theme-sources;
current-index: AppearanceState.auto-theme-source-index;
selected(i) => {
AppearanceState.auto-theme-source-index = i;
AppearanceState.appearance-select("auto-theme-source", i);
}
}
}
if AppearanceState.theme-is-auto && AppearanceState.auto-theme-source-index == 2: SettingRow {
label: AppearanceState.auto-theme-custom-path != ""
? AppearanceState.auto-theme-custom-path
: @tr("Select Image...");
SecondaryButton {
label: @tr("Select Image...");
clicked => {
AppearanceState.appearance-action("auto-theme-select-image");
}
}
}
if AppearanceState.theme-is-auto && AppearanceState.auto-theme-detected-de != "": SettingRow {
label: @tr("Detected: ") + AppearanceState.auto-theme-detected-de;
description: @tr("Experimental: theme may not match your system exactly.");
}
if AppearanceState.theme-is-auto: SettingRow {
label: @tr("Regenerate");
SecondaryButton {
label: @tr("Regenerate");
enabled: !AppearanceState.auto-theme-generating;
clicked => {
AppearanceState.appearance-action("auto-theme-regenerate");
}
}
}
// Custom-theme editor — only when the appended "Custom" theme is selected.
// "Start from current theme" re-seeds the base from the live palette; the
// "Dark theme" toggle flips polarity; the grouped grid edits each base token
// (the rest of the palette is derived live in Rust on every change).
if AppearanceState.theme-is-custom: SettingRow {
label: @tr("Start from current theme");
description: @tr("Copy the colors of the currently applied theme into the editor as a starting point.");
SecondaryButton {
label: @tr("Use current colors");
clicked => {
AppearanceState.custom-seed-from-current();
}
}
}
if AppearanceState.theme-is-custom: SettingRow {
label: @tr("Dark theme");
description: @tr("Set the overall light or dark polarity. Affects derived shades, borders and overlays.");
QbzToggle {
checked: AppearanceState.custom-is-dark;
toggled(v) => {
AppearanceState.custom-toggle-dark(v);
}
}
}
if AppearanceState.theme-is-custom: VerticalLayout {
padding-top: 8px;
padding-bottom: 8px;
spacing: 14px;
// Inline swatch grid — groups side by side, cells inline within each
// group; the shared ColorPicker for the open token expands below.
HorizontalLayout {
spacing: 28px;
alignment: start;
VerticalLayout {
spacing: 8px;
alignment: start;
TokenGroupLabel { text: @tr("SURFACES"); }
HorizontalLayout {
spacing: 10px;
alignment: start;
CustomTokenCell { token-key: "surface-main"; label: @tr("Background"); swatch-color: AppearanceState.custom-surface-main; }
CustomTokenCell { token-key: "surface-card"; label: @tr("Surface"); swatch-color: AppearanceState.custom-surface-card; }
CustomTokenCell { token-key: "surface-elevated"; label: @tr("Elevated"); swatch-color: AppearanceState.custom-surface-elevated; }
}
}
VerticalLayout {
spacing: 8px;
alignment: start;
TokenGroupLabel { text: @tr("TEXT"); }
HorizontalLayout {
spacing: 10px;
alignment: start;
CustomTokenCell { token-key: "text-primary"; label: @tr("Text"); swatch-color: AppearanceState.custom-text-primary; }
CustomTokenCell { token-key: "text-secondary"; label: @tr("Secondary text"); swatch-color: AppearanceState.custom-text-secondary; }
}
}
VerticalLayout {
spacing: 8px;
alignment: start;
TokenGroupLabel { text: @tr("ACCENT"); }
HorizontalLayout {
spacing: 10px;
alignment: start;
CustomTokenCell { token-key: "accent"; label: @tr("Accent"); swatch-color: AppearanceState.custom-accent; }
}
}
}
HorizontalLayout {
spacing: 28px;
alignment: start;
VerticalLayout {
spacing: 8px;
alignment: start;
TokenGroupLabel { text: @tr("STATUS"); }
HorizontalLayout {
spacing: 10px;
alignment: start;
CustomTokenCell { token-key: "danger"; label: @tr("Danger"); swatch-color: AppearanceState.custom-danger; }
CustomTokenCell { token-key: "warning"; label: @tr("Warning"); swatch-color: AppearanceState.custom-warning; }
CustomTokenCell { token-key: "success"; label: @tr("Success"); swatch-color: AppearanceState.custom-success; }
}
}
VerticalLayout {
spacing: 8px;
alignment: start;
TokenGroupLabel { text: @tr("OTHER"); }
HorizontalLayout {
spacing: 10px;
alignment: start;
CustomTokenCell { token-key: "border"; label: @tr("Border"); swatch-color: AppearanceState.custom-border; }
CustomTokenCell { token-key: "favorite"; label: @tr("Favorite"); swatch-color: AppearanceState.custom-favorite; }
}
}
}
// Shared picker — one instance for whichever token is open.
if AppearanceState.custom-open-token != "": ColorPicker {
value: AppearanceState.custom-open-token == "surface-main" ? AppearanceState.custom-surface-main
: AppearanceState.custom-open-token == "surface-card" ? AppearanceState.custom-surface-card
: AppearanceState.custom-open-token == "surface-elevated" ? AppearanceState.custom-surface-elevated
: AppearanceState.custom-open-token == "text-primary" ? AppearanceState.custom-text-primary
: AppearanceState.custom-open-token == "text-secondary" ? AppearanceState.custom-text-secondary
: AppearanceState.custom-open-token == "accent" ? AppearanceState.custom-accent
: AppearanceState.custom-open-token == "danger" ? AppearanceState.custom-danger
: AppearanceState.custom-open-token == "warning" ? AppearanceState.custom-warning
: AppearanceState.custom-open-token == "success" ? AppearanceState.custom-success
: AppearanceState.custom-open-token == "border" ? AppearanceState.custom-border
: AppearanceState.custom-favorite;
changed(c) => {
AppearanceState.custom-set-token(AppearanceState.custom-open-token, c);
}
hex-committed(s) => {
AppearanceState.custom-set-token-hex(AppearanceState.custom-open-token, s);
}
}
}
Rectangle { height: 12px; }
Divider { }
Rectangle { height: 12px; }
// ===================================================================
// TYPOGRAPHY & LANGUAGE
// ===================================================================
GroupHeader { text: @tr("TYPOGRAPHY & LANGUAGE"); }
SettingRow {
label: @tr("Language");
QbzSelect {
menu-width: 200px;
options: AppearanceState.languages;
current-index: AppearanceState.language-index;
selected(i) => {
AppearanceState.language-index = i;
AppearanceState.appearance-select("language", i);
}
}
}
// Interface size — the working replacement for the old percentage "UI
// Scale" placeholder that was parked here (commented, unwired). Window
// scale-factor preset via SLINT_SCALE_FACTOR (main.rs); restart to apply.
SettingRow {
label: @tr("Interface size");
description: @tr("Scales the whole interface, like browser zoom. Small fits more content on screen, Large and Extra large improve readability (requires restart)");
QbzSelect {
menu-width: 200px;
options: AppearanceState.ui-scale-options;
current-index: AppearanceState.ui-scale-index;
selected(i) => {
AppearanceState.ui-scale-index = i;
AppearanceState.appearance-select("ui-scale", i);
}
}
}
SettingRow {
label: @tr("Immersive search");
description: @tr("What selecting a result in the Immersive search does. Disabled turns the in-immersive search off.");
QbzSelect {
menu-width: 200px;
options: AppearanceState.immersive-search-actions;
current-index: AppearanceState.immersive-search-action-index;
selected(i) => {
AppearanceState.immersive-search-action-index = i;
AppearanceState.appearance-select("immersive-search-action", i);
}
}
}
SettingRow {
label: @tr("Immersive default view");
description: @tr("Which immersive view opens by default. 'Remember last' restores your last view.");
QbzSelect {
menu-width: 200px;
options: AppearanceState.immersive-default-views;
current-index: AppearanceState.immersive-default-view-index;
selected(i) => {
AppearanceState.immersive-default-view-index = i;
AppearanceState.appearance-select("immersive-default-view", i);
}
}
}
Rectangle { height: 12px; }
Divider { }
Rectangle { height: 12px; }
// ===================================================================
// LIBRARY & VISUALS
// ===================================================================
GroupHeader { text: @tr("LIBRARY & VISUALS"); }
SettingRow {
label: @tr("Show navigation in sidebar");
description: @tr("Move the Discover, Library, Local Library and My QBZ sections out of the header and into the sidebar.");
QbzToggle {
checked: ShellState.nav-in-sidebar;
toggled(v) => {
ShellState.nav-in-sidebar = v;
ShellState.persist-nav(v);
}
}
}
// Compact header nav — only meaningful while the nav lives in the header,
// so the row mounts only when the toggle above is OFF (ADR-010).
if !ShellState.nav-in-sidebar: SettingRow {
label: @tr("Compact header navigation");
description: @tr("Use the icon-only section navigation in the header even while the sidebar is open.");
QbzToggle {
checked: ShellState.nav-header-compact;
toggled(v) => {
ShellState.nav-header-compact = v;
ShellState.persist-nav-compact(v);
}
}
}
// "My QBZ" nav branding — RENAME ONLY (DQ3, owner decision: no custom icon).
// The icon is fixed to QBZ's monochrome (tray) glyph. The label input
// commits on edit (blank coerces to "My QBZ" in Rust); the reset button
// clears the name back to the default. The icon-change backend (pick-icon /
// reset-icon / custom-icon) stays in Rust but is no longer surfaced.
SettingRow {
label: @tr("My QBZ");
description: @tr("Rename the My QBZ hub. Leave the name blank (or hit reset) to restore the default.");
HorizontalLayout {
spacing: 8px;
// Fixed icon preview — QBZ's monochrome glyph, tinted to text.
Rectangle {
width: 34px;
height: 34px;
border-radius: Radius.sm;
border-width: 1px;
border-color: Theme.border-subtle;
background: Theme.surface-elevated;
QbzIcon {
source: @image-url("../assets/icons/qbz-symbolic.svg");
width: 18px;
height: 18px;
tint: Theme.text-secondary;
}
}
// Label input — commits on edit; the placeholder is the default.
VerticalLayout {
alignment: center;
width: 150px;
name-edit := LineEdit {
text: MyQbzBrandingState.label;
placeholder-text: @tr("My QBZ");
// Hotkey-guard probe: a use-site `changed has-focus` on a std-widgets
// input never fires (has-focus is an alias into the inner TextInput),
// so observe it through a local property instead (#619).
property <bool> guard-focused: self.has-focus;
changed guard-focused => { UiFocusState.text-input-focused = self.guard-focused; }
edited(s) => {
MyQbzBrandingState.set-label(s);
}
}
}
// Reset name — a square glyph button (matches the Output-device
// refresh button), clears the custom name to the default.
VerticalLayout {
alignment: center;
reset-name := Rectangle {
width: 34px;
height: 34px;
border-radius: Radius.sm;
border-width: 1px;
border-color: Theme.border-subtle;
background: reset-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
QbzIcon {
width: 16px;
height: 16px;
source: @image-url("../assets/icons/rotate-ccw.svg");
tint: reset-ta.has-hover ? Theme.text-primary : Theme.text-muted;
}
reset-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
MyQbzBrandingState.set-label("");
}
}
}
}
}
}
SettingRow {
label: @tr("Show playlist cover collages in sidebar");
description: @tr("Render a 2×2 thumbnail of track covers next to each playlist. Disable on low-end machines to skip the extra images.");
QbzToggle {
// Wired: this toggle drives the already-functional sidebar collage flag.
checked: SidebarState.playlist-collage;
toggled(v) => {
SidebarState.playlist-collage = v;
AppearanceState.sidebar-playlist-collage = v;
AppearanceState.appearance-bool("sidebar-playlist-collage", v);
}
}
}
SettingRow {
label: @tr("Show album artwork in Local Library track list");
description: @tr("Display the album cover thumbnail next to each track in the Tracks and Folders views. Off by default — large libraries pay a per-row image-decode cost.");
QbzToggle {
checked: AppearanceState.local-library-track-artwork;
toggled(v) => {
AppearanceState.local-library-track-artwork = v;
AppearanceState.appearance-bool("local-library-track-artwork", v);
}
}
}
SettingRow {
label: @tr("Play indicator animation");
description: @tr("Animate the now-playing row with equalizer bars. Off (default) shows a static pause icon with an accent edge mark — lighter on CPU.");
QbzToggle {
checked: AppearanceState.play-indicator-animation;
toggled(v) => {
AppearanceState.play-indicator-animation = v;
AppearanceState.appearance-bool("play-indicator-animation", v);
}
}
}
SettingRow {
label: @tr("Invert swipe navigation direction");
description: @tr("Swap the two-finger touchpad swipe: left goes back, right goes forward.");
QbzToggle {
checked: AppearanceState.invert-swipe-navigation;
toggled(v) => {
AppearanceState.invert-swipe-navigation = v;
AppearanceState.appearance-bool("invert-swipe-navigation", v);
}
}
}
Rectangle { height: 12px; }
Divider { }
Rectangle { height: 12px; }
// ===================================================================
// NOTIFICATIONS
// ===================================================================
GroupHeader { text: @tr("NOTIFICATIONS"); }
SettingRow {
label: @tr("In-app toasts notifications");
QbzToggle {
checked: AppearanceState.in-app-toasts;
toggled(v) => {
AppearanceState.in-app-toasts = v;
ToastState.enabled = v;
AppearanceState.appearance-bool("in-app-toasts", v);
}
}
}
SettingRow {
label: @tr("System Notifications");
QbzToggle {
checked: AppearanceState.system-notifications;
toggled(v) => {
AppearanceState.system-notifications = v;
AppearanceState.appearance-bool("system-notifications", v);
}
}
}
Rectangle { height: 12px; }
Divider { }
Rectangle { height: 12px; }
// ===================================================================
// WINDOW TITLE
// ===================================================================
GroupHeader { text: @tr("WINDOW TITLE"); }
SettingRow {
label: @tr("Show track in window title");
QbzToggle {
checked: AppearanceState.window-title-show;
toggled(v) => {
AppearanceState.window-title-show = v;
AppearanceState.appearance-bool("window-title-show", v);
}
}
}
// Title-template row — hidden per owner request. The window title uses a
// fixed format (see AppWindow.title in app.slint). Kept commented.
/*
if AppearanceState.window-title-show: SettingRow {
unsupported: true;
label: @tr("Title template");
description: @tr("Placeholders: {{artist}}, {{track}}, {{album}}");
HorizontalLayout {
width: 240px;
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
LineEdit {
text: AppearanceState.window-title-template;
placeholder-text: "{artist} - {track}";
// Hotkey-guard probe: a use-site `changed has-focus` on a std-widgets
// input never fires (has-focus is an alias into the inner TextInput),
// so observe it through a local property instead (#619).
property <bool> guard-focused: self.has-focus;
changed guard-focused => { UiFocusState.text-input-focused = self.guard-focused; }
edited(s) => {
AppearanceState.window-title-template = s;
AppearanceState.appearance-text("window-title-template", s);
}
}
}
}
}
*/
if !AppearanceState.is-macos: Rectangle { height: 12px; }
if !AppearanceState.is-macos: Divider { }
if !AppearanceState.is-macos: Rectangle { height: 12px; }
// ===================================================================
// TITLE BAR (custom window chrome — the header IS the title bar: it is
// always present; custom mode adds window controls + drag to it and
// drops the system decorations. Linux defaults to system chrome; macOS
// keeps the native overlay (traffic lights over the header), so this
// whole section is hidden there.)
// ===================================================================
if !AppearanceState.is-macos: GroupHeader { text: @tr("TITLE BAR"); }
if !AppearanceState.is-macos: SettingRow {
label: @tr("Use system title bar");
description: @tr("Keep your system's native window decorations. Turn off to use the QBZ header as the title bar, with its own window controls and drag support. Takes effect after restarting QBZ.");
QbzToggle {
checked: AppearanceState.use-system-title-bar;
toggled(v) => {
AppearanceState.use-system-title-bar = v;
AppearanceState.appearance-bool("use-system-title-bar", v);
}
}
}
// Tiling-WM row: Linux-only — on macOS the native traffic lights are
// always present (overlay or system bar), so a "hide completely"
// frameless mode does not exist there (same gating idiom as the
// WINDOW CONTROLS rows below).
if !AppearanceState.is-macos: SettingRow {
label: @tr("Hide title bar");
description: @tr("Frameless window without window controls or header drag (for tiling window manager users)");
enabled: !AppearanceState.use-system-title-bar;
QbzToggle {
checked: AppearanceState.hide-title-bar;
enabled: !AppearanceState.use-system-title-bar;
toggled(v) => {
AppearanceState.hide-title-bar = v;
AppearanceState.appearance-bool("hide-title-bar", v);
}
}
}
Rectangle { height: 12px; }
Divider { }
Rectangle { height: 12px; }
// ===================================================================
// WINDOW CONTROLS (drawn in-app on Linux custom chrome; macOS keeps
// the native traffic lights so these rows are hidden there)
// ===================================================================
if !AppearanceState.is-macos: GroupHeader { text: @tr("WINDOW CONTROLS"); }
if !AppearanceState.is-macos: SettingRow {
label: @tr("Window controls position");
description: @tr("Place the window control buttons on the left or right side of the title bar");
enabled: !root.tb-locked;
QbzSelect {
menu-width: 160px;
options: AppearanceState.wc-positions;
current-index: AppearanceState.wc-position-index;
enabled: !root.tb-locked;
selected(i) => {
AppearanceState.wc-position-index = i;
AppearanceState.appearance-select("wc-position", i);
}
}
}
if !AppearanceState.is-macos: SettingRow {
label: @tr("Show window controls");
description: @tr("Show minimize, maximize, and close buttons in the title bar. Disable if your window manager handles these.");
enabled: !root.tb-locked;
QbzToggle {
checked: AppearanceState.show-window-controls;
enabled: !root.tb-locked;
toggled(v) => {
AppearanceState.show-window-controls = v;
AppearanceState.appearance-bool("show-window-controls", v);
}
}
}
Rectangle { height: 12px; }
Divider { }
Rectangle { height: 12px; }
// ===================================================================
// PLAYER VIEWS / STARTUP
// ===================================================================
GroupHeader { text: @tr("PLAYER VIEWS"); }
SettingRow {
label: @tr("Show volume +/- buttons");
description: @tr("Add discrete plus and minus buttons next to the volume slider in the player bar.");
QbzToggle {
checked: AppearanceState.show-volume-steppers;
toggled(v) => {
AppearanceState.show-volume-steppers = v;
AppearanceState.appearance-bool("show-volume-steppers", v);
}
}
}
SettingRow {
label: @tr("Mini player default view");
QbzSelect {
menu-width: 200px;
options: AppearanceState.miniplayer-views;
current-index: AppearanceState.miniplayer-view-index;
selected(i) => {
AppearanceState.miniplayer-view-index = i;
AppearanceState.appearance-select("miniplayer-view", i);
}
}
}
SettingRow {
label: @tr("Startup page");
description: @tr("Choose which page to show when the app starts");
QbzSelect {
menu-width: 200px;
options: AppearanceState.startup-pages;
current-index: AppearanceState.startup-page-index;
selected(i) => {
AppearanceState.startup-page-index = i;
AppearanceState.appearance-select("startup-page", i);
}
}
}
SettingRow {
label: @tr("Show Purchases");
description: @tr("Show the Purchases section in the sidebar for browsing and downloading your purchased music");
QbzToggle {
checked: AppearanceState.show-purchases;
toggled(v) => {
AppearanceState.show-purchases = v;
AppearanceState.appearance-bool("show-purchases", v);
}
}
}
// Purchases title-bar placement — moves the Purchases nav entry into the
// custom title bar instead of the sidebar (mirrors Tauri `purchasesInTitlebar`,
// default OFF). Disabled when the custom title bar is hidden or the system
// title bar is in use (no custom title bar to place it in).
SettingRow {
label: @tr("Purchases in title bar");
description: @tr("Place the Purchases entry in the custom title bar instead of the sidebar");
enabled: !root.tb-locked;
QbzToggle {
checked: AppearanceState.nav-tb-purchases;
enabled: !root.tb-locked;
toggled(v) => {
AppearanceState.nav-tb-purchases = v;
AppearanceState.appearance-bool("nav-tb-purchases", v);
}
}
}
Rectangle { height: 12px; }
Divider { }
Rectangle { height: 12px; }
// ===================================================================
// SYSTEM TRAY (macOS: "MENU BAR")
// ===================================================================
GroupHeader { text: AppearanceState.is-macos ? @tr("MENU BAR") : @tr("SYSTEM TRAY"); }
SettingRow {
label: @tr("Enable tray icon");
description: @tr("Show icon in system tray (requires restart)");
QbzToggle {
checked: AppearanceState.tray-enable;
toggled(v) => {
AppearanceState.tray-enable = v;
AppearanceState.appearance-bool("tray-enable", v);
}
}
}
// NOTE: a separate "Minimize to tray" toggle is intentionally absent.
// Redirecting the window-manager minimize button to the tray requires
// owning that button (custom window chrome), which is not built yet, and
// Wayland forbids a client from intercepting the compositor's minimize.
// Until custom chrome lands, the option is HIDDEN (not shown disabled) and
// this row reads "Close to tray". When custom chrome exists, merge the two
// into a single "Minimize/Close to tray" control.
SettingRow {
label: @tr("Close to tray");
description: AppearanceState.is-macos
? @tr("Keep playing in the menu bar instead of quitting when you close the window")
: @tr("Keep playing in the tray instead of quitting when you close the window");
enabled: AppearanceState.tray-enable;
QbzToggle {
checked: AppearanceState.tray-close-to-tray;
enabled: AppearanceState.tray-enable;
toggled(v) => {
AppearanceState.tray-close-to-tray = v;
AppearanceState.appearance-bool("tray-close-to-tray", v);
}
}
}
// macOS-only: switch the activation policy to .accessory while closed to
// the menu bar (no Dock icon). Off = Spotify-style (keep the Dock icon).
SettingRow {
visible: AppearanceState.is-macos;
label: @tr("Hide Dock icon when closed to menu bar");
description: @tr("Run as a menu-bar-only app while the window is closed. Off keeps the Dock icon (like Spotify)");
enabled: AppearanceState.tray-enable && AppearanceState.tray-close-to-tray;
QbzToggle {
checked: AppearanceState.tray-mac-hide-dock;
enabled: AppearanceState.tray-enable && AppearanceState.tray-close-to-tray;
toggled(v) => {
AppearanceState.tray-mac-hide-dock = v;
AppearanceState.appearance-bool("tray-mac-hide-dock", v);
}
}
}
SettingRow {
label: @tr("Tray icon variant");
description: @tr("Pick a mono glyph to match your panel (Plasma, GNOME's permanently dark top bar) or the full colour vinyl logo");
QbzSelect {
menu-width: 160px;
options: AppearanceState.tray-icon-themes;
current-index: AppearanceState.tray-icon-theme-index;
selected(i) => {
AppearanceState.tray-icon-theme-index = i;
AppearanceState.appearance-select("tray-icon-theme", i);
}
}
}
// IMMERSIVE block — hidden per owner request. Code kept commented for
// re-enable; do NOT delete.
/*
Rectangle { height: 12px; }
Divider { }
Rectangle { height: 12px; }
// ===================================================================
// IMMERSIVE
// ===================================================================
GroupHeader { text: @tr("IMMERSIVE"); }
SettingRow {
unsupported: true;
label: @tr("Background effect");
description: @tr("Controls how the blurred artwork background is rendered in immersive mode.");
QbzSelect {
menu-width: 160px;
options: AppearanceState.immersive-background-modes;
current-index: AppearanceState.immersive-background-mode-index;
selected(i) => {
AppearanceState.immersive-background-mode-index = i;
AppearanceState.appearance-select("immersive-background-mode", i);
}
}
}
// Collapsible: Immersive Panels (per-panel enable/disable lives behind
// this header in Tauri; here the header is replicated, body deferred).
CollapsibleHeader {
title: @tr("Immersive Panels");
summary: @tr("Enable or disable individual immersive view panels");
collapsed: AppearanceState.immersive-panels-collapsed;
toggled => {
AppearanceState.immersive-panels-collapsed = !AppearanceState.immersive-panels-collapsed;
}
}
if !AppearanceState.immersive-panels-collapsed: Text {
text: @tr("Disabled panels are hidden from the visualizer menu so you don't land on them by mistake.");
color: Theme.text-muted;
font-size: 12px;
wrap: word-wrap;
}
// Collapsible: Animation Frame Rate (per-panel FPS lives behind this
// header in Tauri; here the header is replicated, body deferred).
CollapsibleHeader {
title: @tr("Animation Frame Rate");
summary: @tr("Per-animation FPS control for immersive mode");
collapsed: AppearanceState.immersive-fps-collapsed;
toggled => {
AppearanceState.immersive-fps-collapsed = !AppearanceState.immersive-fps-collapsed;
}
}
if !AppearanceState.immersive-fps-collapsed: Text {
text: @tr("Configure frame rate per animation. Lower values save CPU. Requires re-entering immersive mode to apply.");
color: Theme.text-muted;
font-size: 12px;
wrap: word-wrap;
}
*/
// ===================================================================
// RENDERER (Linux only — visibility seeded from Rust; macOS is
// always Skia/Metal so the group never shows there)
// ===================================================================
if AppearanceState.renderer-setting-visible: Rectangle { height: 12px; }
if AppearanceState.renderer-setting-visible: Divider { }
if AppearanceState.renderer-setting-visible: Rectangle { height: 12px; }
if AppearanceState.renderer-setting-visible: GroupHeader { text: @tr("RENDERER"); }
if AppearanceState.renderer-setting-visible: SettingRow {
label: @tr("Rendering backend");
description: @tr("Auto picks the best renderer for your graphics hardware. Only change this if the app feels slow or renders incorrectly (requires restart)");
QbzSelect {
menu-width: 200px;
options: AppearanceState.renderer-options;
current-index: AppearanceState.renderer-index;
selected(i) => {
AppearanceState.renderer-index = i;
AppearanceState.appearance-select("renderer", i);
}
}
}
if AppearanceState.renderer-setting-visible: SettingRow {
label: @tr("Preferred GPU");
description: @tr("Which GPU renders the app. On a hybrid laptop, picking the discrete GPU moves the dynamic-background load off the integrated one (cooler, but more power). Auto is recommended (requires restart)");
QbzSelect {
menu-width: 260px;
options: AppearanceState.gpu-power-modes;
current-index: AppearanceState.gpu-power-index;
selected(i) => {
AppearanceState.gpu-power-index = i;
AppearanceState.appearance-select("gpu-power", i);
}
}
}
Rectangle { height: 40px; }
}