revert: roll back titlebar-mode-selector merge

Reverts merge 1be46f1a which introduced the 4-mode titlebar enum
(qbz/system/plasma/hidden), variant-aware rendering (full/stripped),
GDK_BACKEND=x11 coercion for Plasma, and per-mode mounting in +page.

The "stripped strip mounted alongside Plasma SSD" architecture caused
all titlebar modes to render incorrectly in the production binary —
empty space at top, separation gap above the now-playing bar, controls
visible without a backing strip. Reverting restores v1.2.10 behavior:
two booleans (use_system_titlebar + hide_title_bar), no variants, no
GDK coercion. Plasma support is dropped pending a less brittle redesign.

Conflicts in SettingsView.svelte resolved by taking the pre-merge side
for the four titlebar UI blocks; non-titlebar additions in this cycle
(Discord RPC settings, reserve-DAC, offline cache nav) preserved.

cc
This commit is contained in:
vicrodh committed 2026-05-09 17:48:37 -06:00
1 parent f65248a917
commit e3bdbc9ab4
16 files changed
+287 -782

No files matched your search

+3 -25
View File
@@ -10,7 +10,6 @@ use crate::config::playback_preferences::{
use crate::config::tray_settings::TraySettings; use crate::config::tray_settings::TraySettings;
use crate::config::tray_settings::TraySettingsState; use crate::config::tray_settings::TraySettingsState;
use crate::config::window_settings::WindowSettingsState; use crate::config::window_settings::WindowSettingsState;
use crate::config::window_settings::{available_titlebar_modes, is_sandboxed, TitlebarMode};
use crate::core_bridge::CoreBridgeState; use crate::core_bridge::CoreBridgeState;
use crate::AppState; use crate::AppState;
@@ -24,32 +23,11 @@ pub async fn v2_set_api_locale(locale: String, state: State<'_, AppState>) -> Re
} }
#[tauri::command] #[tauri::command]
pub fn v2_get_titlebar_mode( pub fn v2_set_use_system_titlebar(
state: State<'_, WindowSettingsState>, value: bool,
) -> Result<TitlebarMode, String> {
state.get_titlebar_mode()
}
#[tauri::command]
pub fn v2_set_titlebar_mode(
mode: TitlebarMode,
state: State<'_, WindowSettingsState>, state: State<'_, WindowSettingsState>,
) -> Result<(), String> { ) -> Result<(), String> {
state.set_titlebar_mode(mode) state.set_use_system_titlebar(value)
}
#[derive(serde::Serialize)]
pub struct TitlebarModeAvailability {
pub modes: Vec<TitlebarMode>,
pub sandboxed: bool,
}
#[tauri::command]
pub fn v2_available_titlebar_modes() -> TitlebarModeAvailability {
TitlebarModeAvailability {
modes: available_titlebar_modes(),
sandboxed: is_sandboxed(),
}
} }
#[tauri::command] #[tauri::command]
+2 -166
View File
@@ -9,44 +9,9 @@ use serde::{Deserialize, Serialize};
use std::path::Path; use std::path::Path;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
/// Titlebar rendering mode. Controls which decoration source is active and
/// how the custom titlebar component renders (or whether it renders at all).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TitlebarMode {
/// Default — full custom titlebar with drag, resize, dblclick, controls.
Qbz,
/// OS-native chrome (CSD on Wayland, KWin SSD on X11, NSWindow on macOS).
System,
/// KDE-only — KWin SSD via Xwayland + stripped custom strip below for search+nav.
Plasma,
/// No titlebar — for tiling WMs (i3/Hyprland/sway/niri).
Hidden,
}
impl TitlebarMode {
/// Whether this mode requests OS decorations (decorations=true at window creation).
pub fn wants_decorations(self) -> bool {
matches!(self, TitlebarMode::System | TitlebarMode::Plasma)
}
/// Whether this mode requires GDK_BACKEND=x11 to coerce SSD on Wayland.
pub fn wants_xwayland(self) -> bool {
matches!(self, TitlebarMode::Plasma)
}
}
impl Default for TitlebarMode {
fn default() -> Self {
TitlebarMode::Qbz
}
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WindowSettings { pub struct WindowSettings {
/// Use OS native window decorations (legacy — superseded by `titlebar_mode`). /// Use OS native window decorations instead of custom title bar
/// Kept as a write-through shadow of `titlebar_mode in {System, Plasma}` for
/// any consumers that still read it; will be removed in a follow-up.
pub use_system_titlebar: bool, pub use_system_titlebar: bool,
/// Last saved window width (logical pixels, non-maximized) /// Last saved window width (logical pixels, non-maximized)
pub window_width: f64, pub window_width: f64,
@@ -58,8 +23,6 @@ pub struct WindowSettings {
/// Implies building the window transparent on Linux so the corners /// Implies building the window transparent on Linux so the corners
/// can be seen through to the desktop. Requires restart to take effect. /// can be seen through to the desktop. Requires restart to take effect.
pub match_system_window_chrome: bool, pub match_system_window_chrome: bool,
/// Authoritative titlebar mode (replaces `use_system_titlebar`).
pub titlebar_mode: TitlebarMode,
} }
impl Default for WindowSettings { impl Default for WindowSettings {
@@ -70,7 +33,6 @@ impl Default for WindowSettings {
window_height: 800.0, window_height: 800.0,
is_maximized: false, is_maximized: false,
match_system_window_chrome: false, match_system_window_chrome: false,
titlebar_mode: TitlebarMode::Qbz,
} }
} }
} }
@@ -124,26 +86,6 @@ impl WindowSettingsStore {
[], [],
); );
// Add titlebar_mode column. Defaults to NULL so we can detect
// pre-existing rows and migrate from use_system_titlebar.
let _ = conn.execute(
"ALTER TABLE window_settings ADD COLUMN titlebar_mode TEXT",
[],
);
// One-time migration: if titlebar_mode is NULL but use_system_titlebar
// exists, derive mode. This runs at every open but only does work on
// rows where titlebar_mode is still NULL (idempotent).
let _ = conn.execute(
"UPDATE window_settings
SET titlebar_mode = CASE
WHEN use_system_titlebar = 1 THEN 'system'
ELSE 'qbz'
END
WHERE titlebar_mode IS NULL AND id = 1",
[],
);
info!("[WindowSettings] Database initialized"); info!("[WindowSettings] Database initialized");
Ok(Self { conn }) Ok(Self { conn })
@@ -160,32 +102,11 @@ impl WindowSettingsStore {
Self::open_at(base_dir, "window_settings.db") Self::open_at(base_dir, "window_settings.db")
} }
/// Lightweight read-only open for startup (before Tauri state).
/// Opens existing DB without creating tables/running migrations.
pub fn new_readonly() -> Result<Self, String> {
let db_path = dirs::data_dir()
.ok_or("Could not determine data directory")?
.join("qbz")
.join("window_settings.db");
if !db_path.exists() {
return Err("Window settings DB does not exist yet".to_string());
}
let conn = Connection::open_with_flags(
&db_path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.map_err(|e| format!("Failed to open window settings database (readonly): {}", e))?;
Ok(Self { conn })
}
pub fn get_settings(&self) -> Result<WindowSettings, String> { pub fn get_settings(&self) -> Result<WindowSettings, String> {
self.conn self.conn
.query_row( .query_row(
"SELECT use_system_titlebar, window_width, window_height, is_maximized, "SELECT use_system_titlebar, window_width, window_height, is_maximized,
match_system_window_chrome, titlebar_mode match_system_window_chrome
FROM window_settings WHERE id = 1", FROM window_settings WHERE id = 1",
[], [],
|row| { |row| {
@@ -194,13 +115,6 @@ impl WindowSettingsStore {
let window_height: f64 = row.get(2)?; let window_height: f64 = row.get(2)?;
let is_maximized: i32 = row.get(3)?; let is_maximized: i32 = row.get(3)?;
let match_chrome: i32 = row.get(4)?; let match_chrome: i32 = row.get(4)?;
let mode_str: Option<String> = row.get(5)?;
let titlebar_mode = match mode_str.as_deref() {
Some("system") => TitlebarMode::System,
Some("plasma") => TitlebarMode::Plasma,
Some("hidden") => TitlebarMode::Hidden,
_ => TitlebarMode::Qbz,
};
let defaults = WindowSettings::default(); let defaults = WindowSettings::default();
let (w, h) = if is_valid_window_size(window_width, window_height) { let (w, h) = if is_valid_window_size(window_width, window_height) {
(window_width, window_height) (window_width, window_height)
@@ -218,7 +132,6 @@ impl WindowSettingsStore {
window_height: h, window_height: h,
is_maximized: is_maximized != 0, is_maximized: is_maximized != 0,
match_system_window_chrome: match_chrome != 0, match_system_window_chrome: match_chrome != 0,
titlebar_mode,
}) })
}, },
) )
@@ -245,26 +158,6 @@ impl WindowSettingsStore {
Ok(()) Ok(())
} }
pub fn set_titlebar_mode(&self, mode: TitlebarMode) -> Result<(), String> {
let mode_str = match mode {
TitlebarMode::Qbz => "qbz",
TitlebarMode::System => "system",
TitlebarMode::Plasma => "plasma",
TitlebarMode::Hidden => "hidden",
};
let use_system = mode.wants_decorations() as i64;
self.conn
.execute(
"UPDATE window_settings
SET titlebar_mode = ?1, use_system_titlebar = ?2
WHERE id = 1",
params![mode_str, use_system],
)
.map_err(|e| format!("Failed to set titlebar_mode: {}", e))?;
info!("Window titlebar_mode set to {}", mode_str);
Ok(())
}
/// Save the non-maximized window dimensions (called on resize while not maximized). /// Save the non-maximized window dimensions (called on resize while not maximized).
pub fn set_window_size(&self, width: f64, height: f64) -> Result<(), String> { pub fn set_window_size(&self, width: f64, height: f64) -> Result<(), String> {
if !is_valid_window_size(width, height) { if !is_valid_window_size(width, height) {
@@ -369,23 +262,6 @@ impl WindowSettingsState {
.ok_or("Window settings store not initialized")?; .ok_or("Window settings store not initialized")?;
store.set_match_system_window_chrome(value) store.set_match_system_window_chrome(value)
} }
pub fn set_titlebar_mode(&self, mode: TitlebarMode) -> Result<(), String> {
let store = self.store.lock().map_err(|e| format!("lock poisoned: {}", e))?;
store
.as_ref()
.ok_or_else(|| "Window settings store not initialized".to_string())?
.set_titlebar_mode(mode)
}
pub fn get_titlebar_mode(&self) -> Result<TitlebarMode, String> {
let store = self.store.lock().map_err(|e| format!("lock poisoned: {}", e))?;
let settings = store
.as_ref()
.ok_or_else(|| "Window settings store not initialized".to_string())?
.get_settings()?;
Ok(settings.titlebar_mode)
}
} }
/// Check that window dimensions are within a sane range. /// Check that window dimensions are within a sane range.
@@ -430,43 +306,3 @@ pub fn set_match_system_window_chrome(
); );
state.set_match_system_window_chrome(value) state.set_match_system_window_chrome(value)
} }
/// Returns the titlebar modes valid for the current platform/session.
/// `Plasma` is only included on KDE Plasma + Wayland — on other environments
/// the Xwayland trick is unnecessary or non-applicable.
pub fn available_titlebar_modes() -> Vec<TitlebarMode> {
let mut modes = vec![
TitlebarMode::Qbz,
TitlebarMode::System,
TitlebarMode::Hidden,
];
#[cfg(target_os = "linux")]
{
let kde = std::env::var("XDG_CURRENT_DESKTOP")
.map(|d| d.split(':').any(|s| s.eq_ignore_ascii_case("KDE")))
.unwrap_or(false);
let wayland = std::env::var_os("WAYLAND_DISPLAY").is_some()
|| std::env::var("XDG_SESSION_TYPE").as_deref() == Ok("wayland");
if kde && wayland {
modes.push(TitlebarMode::Plasma);
}
}
modes
}
/// Whether the current process is running inside a Linux sandbox
/// (Flatpak or Snap). Used by the frontend to display a sandbox-caveat
/// hint when the user selects Plasma mode in those builds.
pub fn is_sandboxed() -> bool {
#[cfg(target_os = "linux")]
{
std::path::Path::new("/.flatpak-info").exists()
|| std::env::var_os("SNAP").is_some()
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
+9 -18
View File
@@ -542,15 +542,10 @@ fn should_use_main_window_transparency() -> bool {
// Opt-in via persisted settings: "match system window chrome" wants // Opt-in via persisted settings: "match system window chrome" wants
// rounded corners, which need a transparent window so the webview // rounded corners, which need a transparent window so the webview
// background stops painting white outside the radius. Only honor this // background stops painting white outside the radius.
// when the titlebar mode is Qbz — in System/Plasma/Hidden modes the
// chrome is owned by the platform compositor or a separate widget,
// and a transparent window leaks through KDE/system chrome.
if let Ok(store) = config::window_settings::WindowSettingsStore::new() { if let Ok(store) = config::window_settings::WindowSettingsStore::new() {
if let Ok(settings) = store.get_settings() { if let Ok(settings) = store.get_settings() {
if settings.titlebar_mode == config::window_settings::TitlebarMode::Qbz if settings.match_system_window_chrome {
&& settings.match_system_window_chrome
{
return true; return true;
} }
} }
@@ -711,15 +706,14 @@ pub fn run(qconnect_cli_override: Option<bool>) {
let window_settings = config::window_settings::WindowSettingsStore::new() let window_settings = config::window_settings::WindowSettingsStore::new()
.and_then(|store| store.get_settings()) .and_then(|store| store.get_settings())
.unwrap_or_default(); .unwrap_or_default();
let titlebar_mode = window_settings.titlebar_mode;
log::info!( log::info!(
"Window settings: titlebar_mode={:?}, decorations={}, size={}x{}, maximized={}", "Window settings: use_system_titlebar={}, size={}x{}, maximized={}",
titlebar_mode, window_settings.use_system_titlebar,
titlebar_mode.wants_decorations(),
window_settings.window_width as u32, window_settings.window_width as u32,
window_settings.window_height as u32, window_settings.window_height as u32,
window_settings.is_maximized, window_settings.is_maximized,
); );
let use_system_titlebar = window_settings.use_system_titlebar;
let mut saved_win_width = window_settings.window_width; let mut saved_win_width = window_settings.window_width;
let mut saved_win_height = window_settings.window_height; let mut saved_win_height = window_settings.window_height;
let saved_win_maximized = window_settings.is_maximized; let saved_win_maximized = window_settings.is_maximized;
@@ -950,9 +944,8 @@ pub fn run(qconnect_cli_override: Option<bool>) {
.manage(user_data_paths) .manage(user_data_paths)
.setup(move |app| { .setup(move |app| {
log::info!( log::info!(
"Creating main window (mode={:?}, decorations={})", "Creating main window (decorations={})",
titlebar_mode, use_system_titlebar
titlebar_mode.wants_decorations()
); );
let main_window_transparent = should_use_main_window_transparency(); let main_window_transparent = should_use_main_window_transparency();
MAIN_WINDOW_TRANSPARENT.store(main_window_transparent, std::sync::atomic::Ordering::SeqCst); MAIN_WINDOW_TRANSPARENT.store(main_window_transparent, std::sync::atomic::Ordering::SeqCst);
@@ -1008,7 +1001,7 @@ pub fn run(qconnect_cli_override: Option<bool>) {
.title("QBZ") .title("QBZ")
.inner_size(saved_win_width, saved_win_height) .inner_size(saved_win_width, saved_win_height)
.min_inner_size(800.0, 600.0) .min_inner_size(800.0, 600.0)
.decorations(if cfg!(target_os = "macos") { true } else { titlebar_mode.wants_decorations() }) .decorations(if cfg!(target_os = "macos") { true } else { use_system_titlebar })
.transparent(main_window_transparent) .transparent(main_window_transparent)
.resizable(true) .resizable(true)
.zoom_hotkeys_enabled(true); .zoom_hotkeys_enabled(true);
@@ -1569,9 +1562,7 @@ pub fn run(qconnect_cli_override: Option<bool>) {
updates::has_snap_welcome_been_shown, updates::has_snap_welcome_been_shown,
config::favorites_cache::is_track_favorite, config::favorites_cache::is_track_favorite,
commands_v2::v2_set_api_locale, commands_v2::v2_set_api_locale,
commands_v2::v2_get_titlebar_mode, commands_v2::v2_set_use_system_titlebar,
commands_v2::v2_set_titlebar_mode,
commands_v2::v2_available_titlebar_modes,
commands_v2::v2_set_match_system_window_chrome, commands_v2::v2_set_match_system_window_chrome,
commands_v2::v2_main_window_is_transparent, commands_v2::v2_main_window_is_transparent,
commands_v2::v2_set_enable_tray, commands_v2::v2_set_enable_tray,
+1 -13
View File
@@ -321,13 +321,6 @@ fn main() {
let force_dmabuf = std::env::var("QBZ_FORCE_DMABUF").as_deref() == Ok("1"); let force_dmabuf = std::env::var("QBZ_FORCE_DMABUF").as_deref() == Ok("1");
let disable_dmabuf = std::env::var("QBZ_DISABLE_DMABUF").as_deref() == Ok("1"); let disable_dmabuf = std::env::var("QBZ_DISABLE_DMABUF").as_deref() == Ok("1");
// Read titlebar mode from window settings store (lightweight read-only).
let titlebar_wants_xwayland = qbz_nix_lib::config::window_settings::WindowSettingsStore::new_readonly()
.ok()
.and_then(|store| store.get_settings().ok())
.map(|s| s.titlebar_mode.wants_xwayland())
.unwrap_or(false);
// Force X11: persistent setting from DB, env var overrides (crash recovery) // Force X11: persistent setting from DB, env var overrides (crash recovery)
let force_x11_db = graphics_db.as_ref().map(|s| s.force_x11).unwrap_or(false); let force_x11_db = graphics_db.as_ref().map(|s| s.force_x11).unwrap_or(false);
let force_x11 = match std::env::var("QBZ_FORCE_X11").as_deref() { let force_x11 = match std::env::var("QBZ_FORCE_X11").as_deref() {
@@ -394,13 +387,8 @@ fn main() {
} }
// --- GDK backend selection --- // --- GDK backend selection ---
if (force_x11 || titlebar_wants_xwayland) && is_wayland { if force_x11 && is_wayland {
qbz_nix_lib::logging::log_startup("[QBZ] Forcing X11 backend on Wayland session"); qbz_nix_lib::logging::log_startup("[QBZ] Forcing X11 backend on Wayland session");
if titlebar_wants_xwayland && !force_x11 {
qbz_nix_lib::logging::log_startup(
"[QBZ] X11 backend forced by Plasma titlebar mode",
);
}
std::env::set_var("GDK_BACKEND", "x11"); std::env::set_var("GDK_BACKEND", "x11");
// GDK_SCALE is integer-only and only meaningful on X11/XWayland // GDK_SCALE is integer-only and only meaningful on X11/XWayland
+11 -50
View File
@@ -4,14 +4,8 @@
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { t } from '$lib/i18n'; import { t } from '$lib/i18n';
import { platform } from '$lib/utils/platform';
import type { ButtonColorSet } from '$lib/stores/windowControlsStore'; import type { ButtonColorSet } from '$lib/stores/windowControlsStore';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import {
getTitleBarVariant,
getEffectiveShowWindowControls,
subscribe as subscribeTitleBar,
} from '$lib/stores/titleBarStore';
interface TraySettings { interface TraySettings {
enable_tray: boolean; enable_tray: boolean;
@@ -56,17 +50,6 @@
let appWindow: ReturnType<typeof getCurrentWindow>; let appWindow: ReturnType<typeof getCurrentWindow>;
let searchInputEl = $state<HTMLInputElement | null>(null); let searchInputEl = $state<HTMLInputElement | null>(null);
let variant = $state<'full' | 'stripped'>(getTitleBarVariant());
let effectiveShowControls = $state(getEffectiveShowWindowControls());
$effect(() => {
const unsub = subscribeTitleBar(() => {
variant = getTitleBarVariant();
effectiveShowControls = getEffectiveShowWindowControls();
});
return unsub;
});
onMount(() => { onMount(() => {
let unlisten: (() => void) | undefined; let unlisten: (() => void) | undefined;
@@ -238,20 +221,16 @@
class:has-search={searchInTitlebar} class:has-search={searchInTitlebar}
class:has-nav={!!navSnippet} class:has-nav={!!navSnippet}
class:controls-left={controlsPosition === 'left'} class:controls-left={controlsPosition === 'left'}
class:variant-full={variant === 'full'}
class:variant-stripped={variant === 'stripped'}
class:macos={platform === 'macos'}
{...variant === 'full' ? { 'data-tauri-drag-region': '' } : {}}
> >
<!-- Left zone --> <!-- Left zone -->
<div class="zone zone-left"> <div class="zone zone-left">
{#if effectiveShowControls && controlsPosition === 'left'} {#if showWindowControls && controlsPosition === 'left'}
{@render windowControls('left')} {@render windowControls('left')}
{/if} {/if}
{#if navSnippet && navPosition === 'left'} {#if navSnippet && navPosition === 'left'}
{@render navSnippet()} {@render navSnippet()}
{/if} {/if}
<div class="drag-region" {...variant === 'full' ? { 'data-tauri-drag-region': '' } : {}}></div> <div class="drag-region" data-tauri-drag-region></div>
</div> </div>
<!-- Center zone (search, always centered) --> <!-- Center zone (search, always centered) -->
@@ -285,17 +264,17 @@
{/if} {/if}
</div> </div>
{:else} {:else}
<div class="drag-region" {...variant === 'full' ? { 'data-tauri-drag-region': '' } : {}}></div> <div class="drag-region" data-tauri-drag-region></div>
{/if} {/if}
</div> </div>
<!-- Right zone --> <!-- Right zone -->
<div class="zone zone-right"> <div class="zone zone-right">
<div class="drag-region" {...variant === 'full' ? { 'data-tauri-drag-region': '' } : {}}></div> <div class="drag-region" data-tauri-drag-region></div>
{#if navSnippet && navPosition === 'right'} {#if navSnippet && navPosition === 'right'}
{@render navSnippet()} {@render navSnippet()}
{/if} {/if}
{#if effectiveShowControls && controlsPosition === 'right'} {#if showWindowControls && controlsPosition === 'right'}
{@render windowControls('right')} {@render windowControls('right')}
{/if} {/if}
</div> </div>
@@ -312,30 +291,7 @@
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
-webkit-app-region: drag; -webkit-app-region: drag;
} app-region: drag;
/* Full variant: keeps the default chrome (drag, dblclick, controls). */
.titlebar.variant-full {
height: 44px;
min-height: 44px;
}
/* Stripped variant: KWin SSD owns chrome. No drag, no dblclick, no controls,
no rounded chrome of our own — just the strip below the SSD with search/nav. */
.titlebar.variant-stripped {
height: 42px;
min-height: 42px;
border-radius: 0;
box-shadow: none;
-webkit-app-region: no-drag;
}
/* macOS: TitleBarStyle::Overlay paints the traffic lights at the top-left of
the window, on top of our content. Reserve ~84px on the left zone so the
lights don't collide with nav/search. The reserved area stays a drag
region (it's part of zone-left). */
.titlebar.macos .zone-left {
padding-left: 84px;
} }
/* 3-zone layout: left and right zones are equal width, center is fixed */ /* 3-zone layout: left and right zones are equal width, center is fixed */
@@ -383,6 +339,7 @@
transition: background-color 150ms ease, border-color 150ms ease; transition: background-color 150ms ease, border-color 150ms ease;
flex-shrink: 0; flex-shrink: 0;
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
app-region: no-drag;
color: var(--text-muted); color: var(--text-muted);
} }
@@ -409,6 +366,7 @@
padding: 0; padding: 0;
min-width: 0; min-width: 0;
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
app-region: no-drag;
} }
.titlebar-search-input::placeholder { .titlebar-search-input::placeholder {
@@ -430,6 +388,7 @@
flex-shrink: 0; flex-shrink: 0;
transition: background-color 150ms ease, color 150ms ease; transition: background-color 150ms ease, color 150ms ease;
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
app-region: no-drag;
} }
.titlebar-search-clear:hover { .titlebar-search-clear:hover {
@@ -444,6 +403,7 @@
align-items: stretch; align-items: stretch;
height: 100%; height: 100%;
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
app-region: no-drag;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -492,6 +452,7 @@
cursor: pointer; cursor: pointer;
transition: background-color 150ms ease, color 150ms ease; transition: background-color 150ms ease, color 150ms ease;
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
app-region: no-drag;
} }
/* --- Rectangular --- */ /* --- Rectangular --- */
+77 -152
View File
@@ -99,11 +99,12 @@
} from '$lib/stores/consoleLogStore'; } from '$lib/stores/consoleLogStore';
import { import {
subscribe as subscribeTitleBar, subscribe as subscribeTitleBar,
getMode, getHideTitleBar,
setMode, setHideTitleBar,
getUseSystemTitleBar,
setUseSystemTitleBar,
getShowWindowControls, getShowWindowControls,
setShowWindowControls, setShowWindowControls
type TitlebarMode
} from '$lib/stores/titleBarStore'; } from '$lib/stores/titleBarStore';
import { import {
subscribe as subscribeWindowChrome, subscribe as subscribeWindowChrome,
@@ -1084,43 +1085,11 @@
let windowTitleTemplate = $state(DEFAULT_WINDOW_TITLE_TEMPLATE); let windowTitleTemplate = $state(DEFAULT_WINDOW_TITLE_TEMPLATE);
// Title bar settings // Title bar settings
const initialTitlebarMode: TitlebarMode = getMode(); let hideTitleBar = $state(getHideTitleBar());
let titlebarMode = $state<TitlebarMode>(initialTitlebarMode); let useSystemTitleBar = $state(getUseSystemTitleBar());
// Fallback list before v2_available_titlebar_modes resolves. Always include
// the currently active mode so the dropdown shows the user's selected value
// even when the backend invoke fails.
let availableModes = $state<TitlebarMode[]>(
initialTitlebarMode === 'plasma' ? ['qbz', 'system', 'plasma', 'hidden'] : ['qbz', 'system', 'hidden']
);
let isSandboxed = $state(false);
let matchSystemWindowChromeState = $state(getMatchSystemWindowChrome()); let matchSystemWindowChromeState = $state(getMatchSystemWindowChrome());
let windowControlsVisible = $state(getShowWindowControls()); let windowControlsVisible = $state(getShowWindowControls());
$effect(() => {
invoke<{ modes: TitlebarMode[]; sandboxed: boolean }>('v2_available_titlebar_modes')
.then((result) => {
availableModes = result.modes;
isSandboxed = result.sandboxed;
})
.catch((e) => console.error('[SettingsView] available_titlebar_modes failed:', e));
});
// Per ADR-001: do NOT call $t() inside $derived(). Translate in markup.
function getModeLabel(m: TitlebarMode): string {
return $t(`settings.titlebar.mode.${m}`);
}
function getModeFromLabel(label: string): TitlebarMode | null {
const found = availableModes.find((m) => getModeLabel(m) === label);
return found ?? null;
}
function getDisabledHint(setting: 'windowControls' | 'matchSystemChrome' | 'searchLocation' | 'titlebarNav'): string {
return $t(`settings.titlebar.disabledHint.${setting}`);
}
// Applicability helpers (orthogonal toggles)
const isQbzMode = $derived(titlebarMode === 'qbz');
const supportsTitlebarFeatures = $derived(titlebarMode !== 'hidden');
// Desktop theme detection (Plasma / Klassy → adaptive preset visibility). // Desktop theme detection (Plasma / Klassy → adaptive preset visibility).
// `null` until the first detect call returns. `isKlassy=true` means a // `null` until the first detect call returns. `isKlassy=true` means a
// genuine Klassy install was detected; `desktop` starting with "plasma" // genuine Klassy install was detected; `desktop` starting with "plasma"
@@ -1817,7 +1786,8 @@
// Subscribe to title bar state changes // Subscribe to title bar state changes
const unsubscribeTitleBar = subscribeTitleBar(() => { const unsubscribeTitleBar = subscribeTitleBar(() => {
titlebarMode = getMode(); hideTitleBar = getHideTitleBar();
useSystemTitleBar = getUseSystemTitleBar();
windowControlsVisible = getShowWindowControls(); windowControlsVisible = getShowWindowControls();
}); });
const unsubscribeWindowChrome = subscribeWindowChrome(() => { const unsubscribeWindowChrome = subscribeWindowChrome(() => {
@@ -4836,70 +4806,49 @@
/> />
</div> </div>
{/if} {/if}
<!-- macOS: only the 'qbz' ↔ 'hidden' axis is meaningful. 'system' / <!-- Title bar toggles: hidden on macOS (always uses native overlay title bar) -->
'plasma' describe Linux WM behaviors that don't apply on Darwin, {#if platform !== 'macos'}
so we expose a single Toggle instead of the full mode dropdown. --> <div class="setting-row">
{#if platform === 'macos'} <div class="setting-info">
<span class="setting-label">{$t('settings.appearance.useSystemTitleBar')}</span>
<span class="setting-desc">{$t('settings.appearance.useSystemTitleBarDesc')}</span>
</div>
<Toggle enabled={useSystemTitleBar} onchange={(v) => setUseSystemTitleBar(v)} />
</div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">{$t('settings.appearance.hideTitleBar')}</span> <span class="setting-label">{$t('settings.appearance.hideTitleBar')}</span>
<span class="setting-desc">{$t('settings.appearance.hideTitleBarDesc')}</span> <span class="setting-desc">{$t('settings.appearance.hideTitleBarDesc')}</span>
</div> </div>
<Toggle <Toggle enabled={hideTitleBar} onchange={(v) => setHideTitleBar(v)} disabled={useSystemTitleBar} />
enabled={titlebarMode === 'hidden'}
onchange={(v) => setMode(v ? 'hidden' : 'qbz')}
/>
</div> </div>
{/if}
{#if platform !== 'macos'}
<div class="setting-row"> <div class="setting-row">
<div class="setting-info">
<span class="setting-label">{$t('settings.titlebar.mode.label')}</span>
</div>
<Dropdown
value={getModeLabel(titlebarMode)}
options={availableModes.map((m) => getModeLabel(m))}
onchange={(label) => {
const next = getModeFromLabel(label);
if (next) setMode(next);
}}
/>
</div>
{#if titlebarMode === 'plasma' && isSandboxed}
<p class="hint">{$t('settings.titlebar.sandboxCaveat')}</p>
{/if}
<div class="setting-row" class:disabled-section={!isQbzMode}>
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">{$t('settings.appearance.matchSystemChrome')}</span> <span class="setting-label">{$t('settings.appearance.matchSystemChrome')}</span>
<span class="setting-desc">{$t('settings.appearance.matchSystemChromeDesc')}</span> <span class="setting-desc">{$t('settings.appearance.matchSystemChromeDesc')}</span>
</div> </div>
<span title={!isQbzMode ? getDisabledHint('matchSystemChrome') : ''}> <Toggle
<Toggle enabled={matchSystemWindowChromeState}
enabled={matchSystemWindowChromeState} onchange={(v) => {
onchange={(v) => { setMatchSystemWindowChrome(v);
setMatchSystemWindowChrome(v); showToast($t('settings.appearance.matchSystemChromeRestart'), 'info');
showToast($t('settings.appearance.matchSystemChromeRestart'), 'info'); }}
}} disabled={hideTitleBar || useSystemTitleBar}
disabled={!isQbzMode} />
/>
</span>
</div> </div>
{/if} {/if}
<!-- Search-in-titlebar and nav-in-titlebar: now available on macOS too, <!-- Title bar customization: hidden on macOS (uses native overlay title bar) -->
since the qbz strip mounts there and can host them next to the {#if platform !== 'macos'}
native traffic lights. --> <div class="setting-row">
<div class="setting-row" class:disabled-section={!supportsTitlebarFeatures}>
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">{$t('settings.appearance.searchInTitleBar')}</span> <span class="setting-label">{$t('settings.appearance.searchInTitleBar')}</span>
<span class="setting-desc">{$t('settings.appearance.searchInTitleBarDesc')}</span> <span class="setting-desc">{$t('settings.appearance.searchInTitleBarDesc')}</span>
</div> </div>
<span title={!supportsTitlebarFeatures ? getDisabledHint('searchLocation') : ''}> <Toggle
<Toggle enabled={searchInTitlebar}
enabled={searchInTitlebar} onchange={(v) => setSearchBarLocation(v ? 'titlebar' : 'sidebar')}
onchange={(v) => setSearchBarLocation(v ? 'titlebar' : 'sidebar')} disabled={hideTitleBar || useSystemTitleBar}
disabled={!supportsTitlebarFeatures} />
/>
</span>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
@@ -4907,57 +4856,47 @@
<span class="setting-desc">{$t('settings.appearance.navInTitleBarDesc')}</span> <span class="setting-desc">{$t('settings.appearance.navInTitleBarDesc')}</span>
</div> </div>
</div> </div>
<div class="setting-row indented-setting" class:disabled-section={!supportsTitlebarFeatures}> <div class="setting-row indented-setting">
<span class="setting-label">{$t('nav.home')}</span> <span class="setting-label">{$t('nav.home')}</span>
<span title={!supportsTitlebarFeatures ? getDisabledHint('titlebarNav') : ''}> <Toggle
<Toggle enabled={tbNavConfig.discover}
enabled={tbNavConfig.discover} onchange={(v) => setDiscoverInTitlebar(v)}
onchange={(v) => setDiscoverInTitlebar(v)} disabled={hideTitleBar || useSystemTitleBar}
disabled={!supportsTitlebarFeatures} />
/>
</span>
</div> </div>
<div class="setting-row indented-setting" class:disabled-section={!supportsTitlebarFeatures}> <div class="setting-row indented-setting">
<span class="setting-label">{$t('nav.favorites')}</span> <span class="setting-label">{$t('nav.favorites')}</span>
<span title={!supportsTitlebarFeatures ? getDisabledHint('titlebarNav') : ''}> <Toggle
<Toggle enabled={tbNavConfig.favorites}
enabled={tbNavConfig.favorites} onchange={(v) => setFavoritesInTitlebar(v)}
onchange={(v) => setFavoritesInTitlebar(v)} disabled={hideTitleBar || useSystemTitleBar}
disabled={!supportsTitlebarFeatures} />
/>
</span>
</div> </div>
<div class="setting-row indented-setting" class:disabled-section={!supportsTitlebarFeatures}> <div class="setting-row indented-setting">
<span class="setting-label">{$t('library.title')}</span> <span class="setting-label">{$t('library.title')}</span>
<span title={!supportsTitlebarFeatures ? getDisabledHint('titlebarNav') : ''}> <Toggle
<Toggle enabled={tbNavConfig.library}
enabled={tbNavConfig.library} onchange={(v) => setLibraryInTitlebar(v)}
onchange={(v) => setLibraryInTitlebar(v)} disabled={hideTitleBar || useSystemTitleBar}
disabled={!supportsTitlebarFeatures} />
/>
</span>
</div> </div>
<div class="setting-row indented-setting" class:disabled-section={!supportsTitlebarFeatures}> <div class="setting-row indented-setting">
<span class="setting-label">{$t('nav.myQbz')}</span> <span class="setting-label">{$t('nav.myQbz')}</span>
<span title={!supportsTitlebarFeatures ? getDisabledHint('titlebarNav') : ''}> <Toggle
<Toggle enabled={tbNavConfig.myQbz}
enabled={tbNavConfig.myQbz} onchange={(v) => setMyQbzInTitlebar(v)}
onchange={(v) => setMyQbzInTitlebar(v)} disabled={hideTitleBar || useSystemTitleBar}
disabled={!supportsTitlebarFeatures} />
/>
</span>
</div> </div>
<div class="setting-row indented-setting" class:disabled-section={!supportsTitlebarFeatures}> <div class="setting-row indented-setting">
<span class="setting-label">{$t('nav.purchases')}</span> <span class="setting-label">{$t('nav.purchases')}</span>
<span title={!supportsTitlebarFeatures ? getDisabledHint('titlebarNav') : ''}> <Toggle
<Toggle enabled={tbNavConfig.purchases}
enabled={tbNavConfig.purchases} onchange={(v) => setPurchasesInTitlebar(v)}
onchange={(v) => setPurchasesInTitlebar(v)} disabled={hideTitleBar || useSystemTitleBar}
disabled={!supportsTitlebarFeatures} />
/>
</span>
</div> </div>
{#if titlebarNavAnyEnabled && supportsTitlebarFeatures} {#if titlebarNavAnyEnabled && !hideTitleBar && !useSystemTitleBar}
<div class="setting-row"> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">{$t('settings.appearance.navInTitleBarPosition')}</span> <span class="setting-label">{$t('settings.appearance.navInTitleBarPosition')}</span>
@@ -4980,11 +4919,7 @@
/> />
</div> </div>
{/if} {/if}
<!-- Window-controls customization: still Linux-only. macOS draws native <div class="setting-row" class:disabled-section={hideTitleBar || useSystemTitleBar}>
traffic lights via TitleBarStyle::Overlay, so position/style/size/
color settings would have no effect. -->
{#if platform !== 'macos'}
<div class="setting-row" class:disabled-section={!isQbzMode}>
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">{$t('settings.appearance.windowControlsPosition')}</span> <span class="setting-label">{$t('settings.appearance.windowControlsPosition')}</span>
<span class="setting-desc">{$t('settings.appearance.windowControlsPositionDesc')}</span> <span class="setting-desc">{$t('settings.appearance.windowControlsPositionDesc')}</span>
@@ -4995,7 +4930,7 @@
onchange={handleWcPositionChange} onchange={handleWcPositionChange}
/> />
</div> </div>
<div class="setting-row" class:disabled-section={!isQbzMode}> <div class="setting-row" class:disabled-section={hideTitleBar || useSystemTitleBar}>
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">{$t('settings.appearance.windowControlsStyle')}</span> <span class="setting-label">{$t('settings.appearance.windowControlsStyle')}</span>
<span class="setting-desc">{$t('settings.appearance.windowControlsStyleDesc')}</span> <span class="setting-desc">{$t('settings.appearance.windowControlsStyleDesc')}</span>
@@ -5006,7 +4941,7 @@
onchange={handleWcShapeChange} onchange={handleWcShapeChange}
/> />
</div> </div>
<div class="setting-row" class:disabled-section={!isQbzMode}> <div class="setting-row" class:disabled-section={hideTitleBar || useSystemTitleBar}>
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">{$t('settings.appearance.windowControlsSize')}</span> <span class="setting-label">{$t('settings.appearance.windowControlsSize')}</span>
<span class="setting-desc">{$t('settings.appearance.windowControlsSizeDesc')}</span> <span class="setting-desc">{$t('settings.appearance.windowControlsSizeDesc')}</span>
@@ -5017,7 +4952,7 @@
onchange={handleWcSizeChange} onchange={handleWcSizeChange}
/> />
</div> </div>
<div class="setting-row" class:disabled-section={!isQbzMode}> <div class="setting-row" class:disabled-section={hideTitleBar || useSystemTitleBar}>
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">{$t('settings.appearance.windowControlsColorPreset')}</span> <span class="setting-label">{$t('settings.appearance.windowControlsColorPreset')}</span>
<span class="setting-desc">{$t('settings.appearance.windowControlsColorPresetDesc')}</span> <span class="setting-desc">{$t('settings.appearance.windowControlsColorPresetDesc')}</span>
@@ -5029,7 +4964,7 @@
/> />
</div> </div>
{#if wcConfig.preset === 'custom'} {#if wcConfig.preset === 'custom'}
<div class="wc-custom-panel" class:disabled-section={!isQbzMode}> <div class="wc-custom-panel" class:disabled-section={hideTitleBar || useSystemTitleBar}>
<span class="wc-custom-panel-title">{$t('settings.appearance.windowControlsCustomColors')}</span> <span class="wc-custom-panel-title">{$t('settings.appearance.windowControlsCustomColors')}</span>
{#each WC_BUTTONS as btn} {#each WC_BUTTONS as btn}
<div class="wc-color-group"> <div class="wc-color-group">
@@ -5057,18 +4992,16 @@
{/each} {/each}
</div> </div>
{/if} {/if}
<div class="setting-row" class:disabled-section={!isQbzMode}> <div class="setting-row">
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">{$t('settings.appearance.showWindowControls')}</span> <span class="setting-label">{$t('settings.appearance.showWindowControls')}</span>
<span class="setting-desc">{$t('settings.appearance.showWindowControlsDesc')}</span> <span class="setting-desc">{$t('settings.appearance.showWindowControlsDesc')}</span>
</div> </div>
<span title={!isQbzMode ? getDisabledHint('windowControls') : ''}> <Toggle
<Toggle enabled={windowControlsVisible}
enabled={windowControlsVisible} onchange={(v) => setShowWindowControls(v)}
onchange={(v) => setShowWindowControls(v)} disabled={hideTitleBar || useSystemTitleBar}
disabled={!isQbzMode} />
/>
</span>
</div> </div>
{/if} {/if}
<div class="setting-row"> <div class="setting-row">
@@ -7951,14 +7884,6 @@ flatpak override --user --filesystem=/home/USUARIO/Música com.blitzfc.qbz</pre>
pointer-events: none; pointer-events: none;
} }
/* Inline hint paragraph (e.g. sandbox caveat under titlebar mode dropdown) */
.hint {
margin: 0 0 12px 0;
font-size: 0.85rem;
color: var(--text-muted);
line-height: 1.4;
}
/* Window Controls Custom Colors Panel */ /* Window Controls Custom Colors Panel */
.wc-custom-panel { .wc-custom-panel {
margin: 0 0 8px 0; margin: 0 0 8px 0;
-20
View File
@@ -2456,26 +2456,6 @@
"queue": "Warteschlange" "queue": "Warteschlange"
} }
}, },
"titlebar": {
"label": "Fenstertitelleiste",
"mode": {
"label": "Titelleisten-Modus",
"qbz": "QBZ-Titelleiste (Standard)",
"system": "System-Titelleiste",
"plasma": "Plasma-Titelleiste (KDE Wayland)",
"hidden": "Keine Titelleiste (Tiling WM)"
},
"showWindowControls": "Fensterschaltflächen anzeigen (Min/Max/Schließen)",
"matchSystemChrome": "Systemfensterrahmen anpassen",
"searchLocation": "Suchposition",
"disabledHint": {
"windowControls": "Gilt nur, wenn die QBZ-Titelleiste aktiv ist. Die System- und Plasma-Modi deaktivieren dies, da das Betriebssystem dies bereitstellt.",
"matchSystemChrome": "Gilt nur, wenn die QBZ-Titelleiste aktiv ist. Die System- und Plasma-Modi deaktivieren dies, da das Betriebssystem den Rahmen bereitstellt.",
"searchLocation": "Die Suchposition gilt nicht, wenn keine Titelleiste angezeigt wird.",
"titlebarNav": "Die Titelleisten-Navigation gilt nicht, wenn keine Titelleiste angezeigt wird."
},
"sandboxCaveat": "Der Plasma-Modus ist in Sandbox-Builds (Flatpak/Snap) nicht verifiziert. Wenn der System-Rahmen nicht erscheint, wechseln Sie zum System-Modus."
},
"library": { "library": {
"title": "Bibliothek", "title": "Bibliothek",
"fetchArtistImages": "Qobuz™-Künstlerbilder abrufen", "fetchArtistImages": "Qobuz™-Künstlerbilder abrufen",
-20
View File
@@ -2462,26 +2462,6 @@
} }
} }
}, },
"titlebar": {
"label": "Window titlebar",
"mode": {
"label": "Titlebar mode",
"qbz": "QBZ titlebar (default)",
"system": "System titlebar",
"plasma": "Plasma titlebar (KDE Wayland)",
"hidden": "No titlebar (tiling WM)"
},
"showWindowControls": "Show window controls (min/max/close)",
"matchSystemChrome": "Match system window chrome",
"searchLocation": "Search location",
"disabledHint": {
"windowControls": "Only applies when QBZ titlebar is active. System and Plasma modes disable this because the OS provides it.",
"matchSystemChrome": "Only applies when QBZ titlebar is active. System and Plasma modes disable this because the OS provides chrome.",
"searchLocation": "Search location does not apply when no titlebar is shown.",
"titlebarNav": "Titlebar navigation does not apply when no titlebar is shown."
},
"sandboxCaveat": "Plasma mode is unverified in sandboxed builds (Flatpak/Snap). If the system chrome doesn't appear, switch to System mode."
},
"library": { "library": {
"title": "Library", "title": "Library",
"fetchArtistImages": "Fetch Qobuz™ Artist Images", "fetchArtistImages": "Fetch Qobuz™ Artist Images",
-20
View File
@@ -2462,26 +2462,6 @@
} }
} }
}, },
"titlebar": {
"label": "Barra de título",
"mode": {
"label": "Modo de barra de título",
"qbz": "Barra QBZ (predeterminada)",
"system": "Barra del sistema",
"plasma": "Barra Plasma (KDE Wayland)",
"hidden": "Sin barra (gestores tiling)"
},
"showWindowControls": "Mostrar controles de ventana (min/max/cerrar)",
"matchSystemChrome": "Coincidir con el cromado del sistema",
"searchLocation": "Ubicación de búsqueda",
"disabledHint": {
"windowControls": "Solo aplica cuando la barra QBZ está activa. Los modos Sistema y Plasma lo desactivan porque el OS lo provee.",
"matchSystemChrome": "Solo aplica cuando la barra QBZ está activa. Los modos Sistema y Plasma lo desactivan porque el OS provee la decoración.",
"searchLocation": "La ubicación de búsqueda no aplica cuando no se muestra ninguna barra.",
"titlebarNav": "La navegación de la barra no aplica cuando no se muestra ninguna barra."
},
"sandboxCaveat": "El modo Plasma no está verificado en builds sandboxed (Flatpak/Snap). Si no aparece el cromado del sistema, cambia a modo Sistema."
},
"library": { "library": {
"title": "Biblioteca", "title": "Biblioteca",
"fetchArtistImages": "Obtener imágenes de artistas de Qobuz™", "fetchArtistImages": "Obtener imágenes de artistas de Qobuz™",
-20
View File
@@ -2456,26 +2456,6 @@
} }
} }
}, },
"titlebar": {
"label": "Barre de titre",
"mode": {
"label": "Mode de barre de titre",
"qbz": "Barre QBZ (par défaut)",
"system": "Barre système",
"plasma": "Barre Plasma (KDE Wayland)",
"hidden": "Aucune barre (WM en mosaïque)"
},
"showWindowControls": "Afficher les contrôles de fenêtre (min/max/fermer)",
"matchSystemChrome": "Aligner sur le chrome du système",
"searchLocation": "Emplacement de la recherche",
"disabledHint": {
"windowControls": "Ne s'applique qu'avec la barre QBZ active. Les modes Système et Plasma désactivent ceci car le système d'exploitation le fournit.",
"matchSystemChrome": "Ne s'applique qu'avec la barre QBZ active. Les modes Système et Plasma désactivent ceci car le système d'exploitation fournit le chrome.",
"searchLocation": "L'emplacement de la recherche ne s'applique pas lorsqu'aucune barre n'est affichée.",
"titlebarNav": "La navigation de la barre ne s'applique pas lorsqu'aucune barre n'est affichée."
},
"sandboxCaveat": "Le mode Plasma n'est pas vérifié sur les builds sandboxés (Flatpak/Snap). Si le chrome système n'apparaît pas, basculez vers le mode Système."
},
"library": { "library": {
"title": "Bibliothèque", "title": "Bibliothèque",
"fetchArtistImages": "Récupérer les images d'artistes Qobuz™", "fetchArtistImages": "Récupérer les images d'artistes Qobuz™",
-20
View File
@@ -2456,26 +2456,6 @@
} }
} }
}, },
"titlebar": {
"label": "Barra de título",
"mode": {
"label": "Modo da barra de título",
"qbz": "Barra QBZ (padrão)",
"system": "Barra do sistema",
"plasma": "Barra Plasma (KDE Wayland)",
"hidden": "Sem barra (WM em mosaico)"
},
"showWindowControls": "Mostrar controles de janela (min/máx/fechar)",
"matchSystemChrome": "Combinar com o cromado do sistema",
"searchLocation": "Localização da pesquisa",
"disabledHint": {
"windowControls": "Apenas se aplica quando a barra QBZ está ativa. Os modos Sistema e Plasma desativam isto porque o SO o fornece.",
"matchSystemChrome": "Apenas se aplica quando a barra QBZ está ativa. Os modos Sistema e Plasma desativam isto porque o SO fornece o cromado.",
"searchLocation": "A localização da pesquisa não se aplica quando nenhuma barra é mostrada.",
"titlebarNav": "A navegação da barra não se aplica quando nenhuma barra é mostrada."
},
"sandboxCaveat": "O modo Plasma não está verificado em builds sandboxed (Flatpak/Snap). Se o cromado do sistema não aparecer, mude para o modo Sistema."
},
"library": { "library": {
"title": "Biblioteca", "title": "Biblioteca",
"fetchArtistImages": "Buscar Imagens de Artistas do Qobuz™", "fetchArtistImages": "Buscar Imagens de Artistas do Qobuz™",
+2 -5
View File
@@ -17,7 +17,7 @@ import {
setMiniPlayerSurface, setMiniPlayerSurface,
type MiniPlayerSurface type MiniPlayerSurface
} from '$lib/stores/uiStore'; } from '$lib/stores/uiStore';
import { getMode } from '$lib/stores/titleBarStore'; import { getUseSystemTitleBar } from '$lib/stores/titleBarStore';
import { getUserItem } from '$lib/utils/userStorage'; import { getUserItem } from '$lib/utils/userStorage';
const MINI_PLAYER_LABEL = 'miniplayer'; const MINI_PLAYER_LABEL = 'miniplayer';
@@ -93,10 +93,7 @@ async function getOrCreateMiniPlayerWindow(): Promise<Window> {
minWidth: 340, minWidth: 340,
minHeight: 57, minHeight: 57,
resizable: true, resizable: true,
decorations: (() => { decorations: getUseSystemTitleBar(),
const mode = getMode();
return mode === 'system' || mode === 'plasma';
})(),
alwaysOnTop: miniState.alwaysOnTop, alwaysOnTop: miniState.alwaysOnTop,
transparent: true, transparent: true,
focus: true focus: true
-14
View File
@@ -74,17 +74,3 @@ export function setSearchBarLocation(value: SearchBarLocation): void {
export function toggleSearchBarLocation(): void { export function toggleSearchBarLocation(): void {
setSearchBarLocation(location === 'sidebar' ? 'titlebar' : 'sidebar'); setSearchBarLocation(location === 'sidebar' ? 'titlebar' : 'sidebar');
} }
import { getMode } from './titleBarStore';
/**
* Effective search-bar location after applying mode rules.
* Only 'hidden' forces sidebar; qbz/plasma/system all honor user pref.
* In system mode the stripped strip mounts when the user wants
* search-in-titlebar (same content rule as plasma).
*/
export function getEffectiveSearchBarLocation(): SearchBarLocation {
const mode = getMode();
if (mode === 'hidden') return 'sidebar';
return getSearchBarLocation();
}
+178 -211
View File
@@ -1,211 +1,178 @@
/** /**
* Title Bar Settings Store * Title Bar Settings Store
* *
* Manages titlebar mode (single 4-value enum) plus the orthogonal * Manages title bar visibility settings with localStorage persistence.
* `showWindowControls` preference. *
* * Settings:
* Modes: * - hideTitleBar: Remove title bar completely for tiling WM users (default: false)
* - 'qbz': default — full custom titlebar with drag/resize/dblclick/controls * - useSystemTitleBar: Use OS native window decorations instead of custom title bar (default: false)
* - 'system': OS-native chrome */
* - 'plasma': KDE-only, KWin SSD via Xwayland + stripped strip
* - 'hidden': no titlebar (tiling WMs) import { invoke } from '@tauri-apps/api/core';
*/ import { skipIfRemote } from '$lib/services/commandRouter';
import { platform } from '$lib/utils/platform';
import { invoke } from '@tauri-apps/api/core';
import { skipIfRemote } from '$lib/services/commandRouter'; const STORAGE_KEY_HIDE = 'qbz-hide-titlebar';
import { platform } from '$lib/utils/platform'; const STORAGE_KEY_SYSTEM = 'qbz-use-system-titlebar';
import { getSearchBarLocation } from './searchBarLocationStore'; const STORAGE_KEY_WINDOW_CONTROLS = 'qbz-show-window-controls';
import { isTitlebarNavEnabled } from './titlebarNavStore';
// State
export type TitlebarMode = 'qbz' | 'system' | 'plasma' | 'hidden'; let hideTitleBar = false;
let useSystemTitleBar = false;
const STORAGE_KEY_MODE = 'qbz-titlebar-mode'; let showWindowControls = true;
const STORAGE_KEY_WINDOW_CONTROLS = 'qbz-show-window-controls';
const LEGACY_KEY_HIDE = 'qbz-hide-titlebar'; // Listeners
const LEGACY_KEY_SYSTEM = 'qbz-use-system-titlebar'; const listeners = new Set<() => void>();
let mode: TitlebarMode = 'qbz'; function notifyListeners(): void {
let showWindowControls = true; for (const listener of listeners) {
listener();
const listeners = new Set<() => void>(); }
}
function notifyListeners(): void {
for (const listener of listeners) { /**
listener(); * Initialize the store from localStorage
} */
} export function initTitleBarStore(): void {
if (skipIfRemote()) return;
function migrateLegacyKeys(): TitlebarMode | null { try {
// Returns the migrated mode, or null if no migration was needed. const savedHide = localStorage.getItem(STORAGE_KEY_HIDE);
const hide = localStorage.getItem(LEGACY_KEY_HIDE); if (savedHide !== null) {
const system = localStorage.getItem(LEGACY_KEY_SYSTEM); hideTitleBar = savedHide === 'true';
if (hide === null && system === null) return null; }
let migrated: TitlebarMode = 'qbz'; const savedSystem = localStorage.getItem(STORAGE_KEY_SYSTEM);
if (hide === 'true') { if (savedSystem !== null) {
migrated = 'hidden'; useSystemTitleBar = savedSystem === 'true';
} else if (system === 'true') { }
migrated = 'system';
} const savedControls = localStorage.getItem(STORAGE_KEY_WINDOW_CONTROLS);
return migrated; if (savedControls !== null) {
} showWindowControls = savedControls !== 'false';
}
export function initTitleBarStore(): void { // Sync localStorage value to Rust backend so it's available at next
if (skipIfRemote()) return; // startup (before window creation). Handles migration from the
try { // localStorage-only era and keeps both stores in sync.
const savedMode = localStorage.getItem(STORAGE_KEY_MODE); // Always sync, not just when true - otherwise false values never propagate.
if (savedMode === 'qbz' || savedMode === 'system' || savedMode === 'plasma' || savedMode === 'hidden') { invoke('v2_set_use_system_titlebar', { value: useSystemTitleBar }).catch((e) => {
mode = savedMode; console.error('[TitleBarStore] Failed to sync system titlebar to backend:', e);
} else { });
// No persisted mode — try migrating from legacy keys. } catch (e) {
const migrated = migrateLegacyKeys(); console.error('[TitleBarStore] Failed to initialize:', e);
if (migrated !== null) { }
mode = migrated; }
localStorage.setItem(STORAGE_KEY_MODE, mode);
// Push to backend so next launch reads the correct mode before window creation. /**
invoke('v2_set_titlebar_mode', { mode }).catch((e) => { * Subscribe to title bar state changes
console.error('[TitleBarStore] Failed to persist migrated mode to backend:', e); */
}); export function subscribe(listener: () => void): () => void {
} listeners.add(listener);
} listener();
return () => listeners.delete(listener);
const savedControls = localStorage.getItem(STORAGE_KEY_WINDOW_CONTROLS); }
if (savedControls !== null) {
showWindowControls = savedControls !== 'false'; /**
} * Get current hide setting
} catch (e) { */
console.error('[TitleBarStore] Failed to initialize:', e); export function getHideTitleBar(): boolean {
} return hideTitleBar;
} }
export function subscribe(listener: () => void): () => void { /**
listeners.add(listener); * Get current system title bar setting
listener(); */
return () => listeners.delete(listener); export function getUseSystemTitleBar(): boolean {
} return useSystemTitleBar;
}
export function getMode(): TitlebarMode {
return mode; /**
} * Determine if the custom title bar should be visible
* Hidden when either system title bar is active or hide mode is on
/** */
* Whether the custom TitleBar.svelte component should mount. export function shouldShowTitleBar(): boolean {
* - 'qbz' → true (full variant). macOS always falls here: mode selection // macOS always uses native decorations — no custom title bar
* is hidden in settings on macOS, so it stays at the 'qbz' default and if (platform === 'macos') return false;
* the strip mounts with native traffic lights overlaid on its left zone return !hideTitleBar && !useSystemTitleBar;
* (see TitleBar.svelte's `.titlebar.macos` rule). }
* - 'plasma' or 'system' → true ONLY if the stripped strip would carry
* content (search-in-titlebar OR at least one nav item). Both render /**
* the strip below their respective OS chrome (KWin SSD via Xwayland * Get the title bar height for layout calculations
* for plasma, GTK CSD / WM SSD for system). * Returns 0 if title bar is hidden or system title bar is active, 40 otherwise
* - 'hidden' → false. */
*/ export function getTitleBarHeight(): number {
export function shouldShowTitleBar(): boolean { if (platform === 'macos') return 0;
if (mode === 'hidden') return false; return (hideTitleBar || useSystemTitleBar) ? 0 : 40;
if (mode === 'qbz') return true; }
// mode === 'plasma' or 'system' — both render the stripped strip below
// their respective chrome (KWin SSD or GTK CSD), but only if the strip /**
// would have effective content. * Get whether to show window control buttons (minimize/maximize/close)
return getSearchBarLocation() === 'titlebar' || isTitlebarNavEnabled(); */
} export function getShowWindowControls(): boolean {
return showWindowControls;
/** }
* Variant for the custom TitleBar component when mounted.
* 'qbz' is the full variant; everything else (plasma/system) is the /**
* stripped strip rendered below OS chrome. * Set whether to show window control buttons
*/ */
export function getTitleBarVariant(): 'full' | 'stripped' { export function setShowWindowControls(value: boolean): void {
return mode === 'qbz' ? 'full' : 'stripped'; showWindowControls = value;
} try {
localStorage.setItem(STORAGE_KEY_WINDOW_CONTROLS, String(value));
export function getTitleBarHeight(): number { } catch (e) {
if (!shouldShowTitleBar()) return 0; console.error('[TitleBarStore] Failed to save window controls setting:', e);
return mode === 'qbz' ? 44 : 42; }
} notifyListeners();
}
export function getShowWindowControls(): boolean {
return showWindowControls; /**
} * Set whether to hide title bar completely
*/
export function setShowWindowControls(value: boolean): void { export function setHideTitleBar(value: boolean): void {
showWindowControls = value; hideTitleBar = value;
try { try {
localStorage.setItem(STORAGE_KEY_WINDOW_CONTROLS, String(value)); localStorage.setItem(STORAGE_KEY_HIDE, String(value));
} catch (e) { } catch (e) {
console.error('[TitleBarStore] Failed to save window controls setting:', e); console.error('[TitleBarStore] Failed to save hide titlebar setting:', e);
} }
notifyListeners(); notifyListeners();
} }
/** /**
* Effective `showWindowControls` after applying mode rules. * Set whether to use system (OS native) title bar.
* In 'plasma' mode, KWin draws controls in its SSD, so the custom * Persists to both localStorage and Rust backend, then restarts the app.
* controls are forced off regardless of the user pref. * Restart is required because the window must be created with the correct
*/ * decoration state — runtime toggling doesn't work reliably on Linux/Wayland.
export function getEffectiveShowWindowControls(): boolean { */
// macOS draws native traffic lights via TitleBarStyle::Overlay, so the export async function setUseSystemTitleBar(value: boolean): Promise<void> {
// custom controls would visually collide with them. if (skipIfRemote()) return;
if (platform === 'macos') return false; useSystemTitleBar = value;
if (mode !== 'qbz') return false; try {
return showWindowControls; localStorage.setItem(STORAGE_KEY_SYSTEM, String(value));
} } catch (e) {
console.error('[TitleBarStore] Failed to save system titlebar setting:', e);
/** }
* Set the titlebar mode. Persists to localStorage and backend, then try {
* restarts the app when the mode change crosses the decoration boundary await invoke('v2_set_use_system_titlebar', { value });
* (mounting/unmounting decorations requires window re-creation). await invoke('v2_restart_app');
*/ } catch (e) {
export async function setMode(value: TitlebarMode): Promise<void> { console.error('[TitleBarStore] Failed to apply system titlebar change:', e);
if (skipIfRemote()) return; }
}
const previousWantsDecorations =
mode === 'system' || mode === 'plasma'; export interface TitleBarState {
const newWantsDecorations = hideTitleBar: boolean;
value === 'system' || value === 'plasma'; useSystemTitleBar: boolean;
showTitleBar: boolean;
// Push to backend FIRST. If it fails, leave local state untouched. showWindowControls: boolean;
try { titleBarHeight: number;
await invoke('v2_set_titlebar_mode', { mode: value }); }
} catch (e) {
console.error('[TitleBarStore] Failed to push mode to backend:', e); export function getTitleBarState(): TitleBarState {
return; return {
} hideTitleBar,
useSystemTitleBar,
mode = value; showTitleBar: shouldShowTitleBar(),
try { showWindowControls,
localStorage.setItem(STORAGE_KEY_MODE, value); titleBarHeight: getTitleBarHeight()
} catch (e) { };
console.error('[TitleBarStore] Failed to save mode setting:', e); }
}
if (previousWantsDecorations !== newWantsDecorations) {
// Crossing the decoration boundary requires window re-creation.
try {
await invoke('v2_restart_app');
} catch (e) {
console.error('[TitleBarStore] Failed to restart app:', e);
}
} else {
notifyListeners();
}
}
export interface TitleBarState {
mode: TitlebarMode;
showTitleBar: boolean;
variant: 'full' | 'stripped';
showWindowControls: boolean;
effectiveShowWindowControls: boolean;
titleBarHeight: number;
}
export function getTitleBarState(): TitleBarState {
return {
mode,
showTitleBar: shouldShowTitleBar(),
variant: getTitleBarVariant(),
showWindowControls,
effectiveShowWindowControls: getEffectiveShowWindowControls(),
titleBarHeight: getTitleBarHeight()
};
}
-13
View File
@@ -172,16 +172,3 @@ export function setTitlebarNavConfig(newConfig: Partial<TitlebarNavConfig>): voi
persist(); persist();
notifyListeners(); notifyListeners();
} }
import { getMode } from './titleBarStore';
/**
* Whether the titlebar nav should render.
* Only 'hidden' is N/A; qbz/plasma/system all honor user pref.
* In system mode the stripped strip carries the nav (same as plasma).
*/
export function isTitlebarNavEffectivelyEnabled(): boolean {
const mode = getMode();
if (mode === 'hidden') return false;
return isTitlebarNavEnabled();
}
+4 -15
View File
@@ -123,7 +123,7 @@
import { import {
subscribe as subscribeSearchBarLocation, subscribe as subscribeSearchBarLocation,
initSearchBarLocation, initSearchBarLocation,
getEffectiveSearchBarLocation getSearchBarLocation
} from '$lib/stores/searchBarLocationStore'; } from '$lib/stores/searchBarLocationStore';
// Window controls customization store // Window controls customization store
@@ -551,8 +551,8 @@
let windowTransparent = $state(getWindowIsTransparent()); let windowTransparent = $state(getWindowIsTransparent());
let showWindowControls = $state(getShowWindowControls()); let showWindowControls = $state(getShowWindowControls());
// Search Bar Location State (effective: respects mode, falls back to sidebar in system/hidden) // Search Bar Location State
let searchBarLocationPref = $state(getEffectiveSearchBarLocation()); let searchBarLocationPref = $state(getSearchBarLocation());
let titlebarSearchQuery = $state(getSearchQuery()); let titlebarSearchQuery = $state(getSearchQuery());
// Window Controls State // Window Controls State
@@ -4840,9 +4840,6 @@
const unsubscribeTitleBar = subscribeTitleBar(() => { const unsubscribeTitleBar = subscribeTitleBar(() => {
showTitleBar = shouldShowTitleBar(); showTitleBar = shouldShowTitleBar();
showWindowControls = getShowWindowControls(); showWindowControls = getShowWindowControls();
// Recompute the effective search location: mode changes can flip
// search from titlebar to sidebar even when the user pref is unchanged.
searchBarLocationPref = getEffectiveSearchBarLocation();
applyChromeClass(); applyChromeClass();
}); });
@@ -4877,11 +4874,7 @@
// Initialize and subscribe to search bar location // Initialize and subscribe to search bar location
initSearchBarLocation(); initSearchBarLocation();
const unsubscribeSearchBarLocation = subscribeSearchBarLocation(() => { const unsubscribeSearchBarLocation = subscribeSearchBarLocation(() => {
searchBarLocationPref = getEffectiveSearchBarLocation(); searchBarLocationPref = getSearchBarLocation();
// In plasma mode the strip's visibility depends on whether search
// or any nav item is enabled, so a search-location toggle can flip
// the strip on/off.
showTitleBar = shouldShowTitleBar();
}); });
// Initialize and subscribe to window controls customization // Initialize and subscribe to window controls customization
@@ -4902,10 +4895,6 @@
tbNavLibrary = isLibraryInTitlebar(); tbNavLibrary = isLibraryInTitlebar();
tbNavMyQbz = isMyQbzInTitlebar(); tbNavMyQbz = isMyQbzInTitlebar();
tbNavPurchases = isPurchasesInTitlebar(); tbNavPurchases = isPurchasesInTitlebar();
// In plasma mode the strip's visibility depends on whether any nav
// item or search is enabled, so toggling the last nav item can
// flip the strip on/off.
showTitleBar = shouldShowTitleBar();
}); });
// Detect floating window state (not maximized) for rounded corners + shadow // Detect floating window state (not maximized) for rounded corners + shadow