Repoint the existing auto-theme Settings scaffold from the "System" theme to the new dynamic "Auto (dynamic)" option: - state.slint: add theme-is-auto (distinct from theme-is-system, which stays the native OS-palette follow). - AppearanceSettings.slint: gate the Source dropdown, image picker, detected-DE hint and Regenerate button on theme-is-auto, and drop the placeholder `unsupported` flag now that the rows are functional. - translations: add "Auto theme generation failed" to all 7 catalogs and give the es "Wallpaper Sync" string a proper Mexican-neutral translation. cc
815 lines
32 KiB
Plaintext
815 lines
32 KiB
Plaintext
// 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 { 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;
|
||
}
|
||
|
||
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 {
|
||
source: AppearanceState.theme-filter == 1
|
||
? @image-url("../assets/icons/disc.svg")
|
||
: AppearanceState.theme-filter == 2
|
||
? @image-url("../assets/icons/disc.svg")
|
||
: @image-url("../assets/icons/disc.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: 200px;
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
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");
|
||
}
|
||
}
|
||
}
|
||
|
||
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");
|
||
changed has-focus => { UiFocusState.text-input-focused = self.has-focus; }
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
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}";
|
||
changed has-focus => { UiFocusState.text-input-focused = self.has-focus; }
|
||
edited(s) => {
|
||
AppearanceState.window-title-template = s;
|
||
AppearanceState.appearance-text("window-title-template", s);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
*/
|
||
|
||
Rectangle { height: 12px; }
|
||
Divider { }
|
||
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
|
||
// defaults to the overlay (native traffic lights over the header).)
|
||
// ===================================================================
|
||
GroupHeader { text: @tr("TITLE BAR"); }
|
||
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle { height: 40px; }
|
||
}
|