From e3bdbc9ab40f1d10330d7c11c87ce84cd5d89db4 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Sat, 9 May 2026 17:48:37 -0600 Subject: [PATCH] revert: roll back titlebar-mode-selector merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src-tauri/src/commands_v2/settings.rs | 28 +- src-tauri/src/config/window_settings.rs | 168 +------- src-tauri/src/lib.rs | 27 +- src-tauri/src/main.rs | 14 +- src/lib/components/TitleBar.svelte | 61 +-- src/lib/components/views/SettingsView.svelte | 229 ++++------- src/lib/i18n/locales/de.json | 20 - src/lib/i18n/locales/en.json | 20 - src/lib/i18n/locales/es.json | 20 - src/lib/i18n/locales/fr.json | 20 - src/lib/i18n/locales/pt.json | 20 - src/lib/services/miniplayerService.ts | 7 +- src/lib/stores/searchBarLocationStore.ts | 14 - src/lib/stores/titleBarStore.ts | 389 +++++++++---------- src/lib/stores/titlebarNavStore.ts | 13 - src/routes/+page.svelte | 19 +- 16 files changed, 287 insertions(+), 782 deletions(-) diff --git a/src-tauri/src/commands_v2/settings.rs b/src-tauri/src/commands_v2/settings.rs index 1333fe6f7..9976830ee 100644 --- a/src-tauri/src/commands_v2/settings.rs +++ b/src-tauri/src/commands_v2/settings.rs @@ -10,7 +10,6 @@ use crate::config::playback_preferences::{ use crate::config::tray_settings::TraySettings; use crate::config::tray_settings::TraySettingsState; use crate::config::window_settings::WindowSettingsState; -use crate::config::window_settings::{available_titlebar_modes, is_sandboxed, TitlebarMode}; use crate::core_bridge::CoreBridgeState; use crate::AppState; @@ -24,32 +23,11 @@ pub async fn v2_set_api_locale(locale: String, state: State<'_, AppState>) -> Re } #[tauri::command] -pub fn v2_get_titlebar_mode( - state: State<'_, WindowSettingsState>, -) -> Result { - state.get_titlebar_mode() -} - -#[tauri::command] -pub fn v2_set_titlebar_mode( - mode: TitlebarMode, +pub fn v2_set_use_system_titlebar( + value: bool, state: State<'_, WindowSettingsState>, ) -> Result<(), String> { - state.set_titlebar_mode(mode) -} - -#[derive(serde::Serialize)] -pub struct TitlebarModeAvailability { - pub modes: Vec, - pub sandboxed: bool, -} - -#[tauri::command] -pub fn v2_available_titlebar_modes() -> TitlebarModeAvailability { - TitlebarModeAvailability { - modes: available_titlebar_modes(), - sandboxed: is_sandboxed(), - } + state.set_use_system_titlebar(value) } #[tauri::command] diff --git a/src-tauri/src/config/window_settings.rs b/src-tauri/src/config/window_settings.rs index e60d7e88b..adb57bbb8 100644 --- a/src-tauri/src/config/window_settings.rs +++ b/src-tauri/src/config/window_settings.rs @@ -9,44 +9,9 @@ use serde::{Deserialize, Serialize}; use std::path::Path; 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)] pub struct WindowSettings { - /// Use OS native window decorations (legacy — superseded by `titlebar_mode`). - /// 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. + /// Use OS native window decorations instead of custom title bar pub use_system_titlebar: bool, /// Last saved window width (logical pixels, non-maximized) pub window_width: f64, @@ -58,8 +23,6 @@ pub struct WindowSettings { /// Implies building the window transparent on Linux so the corners /// can be seen through to the desktop. Requires restart to take effect. pub match_system_window_chrome: bool, - /// Authoritative titlebar mode (replaces `use_system_titlebar`). - pub titlebar_mode: TitlebarMode, } impl Default for WindowSettings { @@ -70,7 +33,6 @@ impl Default for WindowSettings { window_height: 800.0, is_maximized: 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"); Ok(Self { conn }) @@ -160,32 +102,11 @@ impl WindowSettingsStore { 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 { - 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 { self.conn .query_row( "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", [], |row| { @@ -194,13 +115,6 @@ impl WindowSettingsStore { let window_height: f64 = row.get(2)?; let is_maximized: i32 = row.get(3)?; let match_chrome: i32 = row.get(4)?; - let mode_str: Option = 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 (w, h) = if is_valid_window_size(window_width, window_height) { (window_width, window_height) @@ -218,7 +132,6 @@ impl WindowSettingsStore { window_height: h, is_maximized: is_maximized != 0, match_system_window_chrome: match_chrome != 0, - titlebar_mode, }) }, ) @@ -245,26 +158,6 @@ impl WindowSettingsStore { 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). pub fn set_window_size(&self, width: f64, height: f64) -> Result<(), String> { if !is_valid_window_size(width, height) { @@ -369,23 +262,6 @@ impl WindowSettingsState { .ok_or("Window settings store not initialized")?; 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 { - 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. @@ -430,43 +306,3 @@ pub fn set_match_system_window_chrome( ); 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 { - 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 - } -} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5b40d1322..c92f16810 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -542,15 +542,10 @@ fn should_use_main_window_transparency() -> bool { // Opt-in via persisted settings: "match system window chrome" wants // rounded corners, which need a transparent window so the webview - // background stops painting white outside the radius. Only honor this - // 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. + // background stops painting white outside the radius. if let Ok(store) = config::window_settings::WindowSettingsStore::new() { if let Ok(settings) = store.get_settings() { - if settings.titlebar_mode == config::window_settings::TitlebarMode::Qbz - && settings.match_system_window_chrome - { + if settings.match_system_window_chrome { return true; } } @@ -711,15 +706,14 @@ pub fn run(qconnect_cli_override: Option) { let window_settings = config::window_settings::WindowSettingsStore::new() .and_then(|store| store.get_settings()) .unwrap_or_default(); - let titlebar_mode = window_settings.titlebar_mode; log::info!( - "Window settings: titlebar_mode={:?}, decorations={}, size={}x{}, maximized={}", - titlebar_mode, - titlebar_mode.wants_decorations(), + "Window settings: use_system_titlebar={}, size={}x{}, maximized={}", + window_settings.use_system_titlebar, window_settings.window_width as u32, window_settings.window_height as u32, 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_height = window_settings.window_height; let saved_win_maximized = window_settings.is_maximized; @@ -950,9 +944,8 @@ pub fn run(qconnect_cli_override: Option) { .manage(user_data_paths) .setup(move |app| { log::info!( - "Creating main window (mode={:?}, decorations={})", - titlebar_mode, - titlebar_mode.wants_decorations() + "Creating main window (decorations={})", + use_system_titlebar ); let main_window_transparent = should_use_main_window_transparency(); MAIN_WINDOW_TRANSPARENT.store(main_window_transparent, std::sync::atomic::Ordering::SeqCst); @@ -1008,7 +1001,7 @@ pub fn run(qconnect_cli_override: Option) { .title("QBZ") .inner_size(saved_win_width, saved_win_height) .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) .resizable(true) .zoom_hotkeys_enabled(true); @@ -1569,9 +1562,7 @@ pub fn run(qconnect_cli_override: Option) { updates::has_snap_welcome_been_shown, config::favorites_cache::is_track_favorite, commands_v2::v2_set_api_locale, - commands_v2::v2_get_titlebar_mode, - commands_v2::v2_set_titlebar_mode, - commands_v2::v2_available_titlebar_modes, + commands_v2::v2_set_use_system_titlebar, commands_v2::v2_set_match_system_window_chrome, commands_v2::v2_main_window_is_transparent, commands_v2::v2_set_enable_tray, diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 99b355544..4b6588855 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -321,13 +321,6 @@ fn main() { 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"); - // 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) 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() { @@ -394,13 +387,8 @@ fn main() { } // --- 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"); - 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"); // GDK_SCALE is integer-only and only meaningful on X11/XWayland diff --git a/src/lib/components/TitleBar.svelte b/src/lib/components/TitleBar.svelte index 952563b63..563df02b4 100644 --- a/src/lib/components/TitleBar.svelte +++ b/src/lib/components/TitleBar.svelte @@ -4,14 +4,8 @@ import { invoke } from '@tauri-apps/api/core'; import { onMount } from 'svelte'; import { t } from '$lib/i18n'; - import { platform } from '$lib/utils/platform'; import type { ButtonColorSet } from '$lib/stores/windowControlsStore'; import type { Snippet } from 'svelte'; - import { - getTitleBarVariant, - getEffectiveShowWindowControls, - subscribe as subscribeTitleBar, - } from '$lib/stores/titleBarStore'; interface TraySettings { enable_tray: boolean; @@ -56,17 +50,6 @@ let appWindow: ReturnType; let searchInputEl = $state(null); - let variant = $state<'full' | 'stripped'>(getTitleBarVariant()); - let effectiveShowControls = $state(getEffectiveShowWindowControls()); - - $effect(() => { - const unsub = subscribeTitleBar(() => { - variant = getTitleBarVariant(); - effectiveShowControls = getEffectiveShowWindowControls(); - }); - return unsub; - }); - onMount(() => { let unlisten: (() => void) | undefined; @@ -238,20 +221,16 @@ class:has-search={searchInTitlebar} class:has-nav={!!navSnippet} 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': '' } : {}} >
- {#if effectiveShowControls && controlsPosition === 'left'} + {#if showWindowControls && controlsPosition === 'left'} {@render windowControls('left')} {/if} {#if navSnippet && navPosition === 'left'} {@render navSnippet()} {/if} -
+
@@ -285,17 +264,17 @@ {/if} {:else} -
+
{/if}
-
+
{#if navSnippet && navPosition === 'right'} {@render navSnippet()} {/if} - {#if effectiveShowControls && controlsPosition === 'right'} + {#if showWindowControls && controlsPosition === 'right'} {@render windowControls('right')} {/if}
@@ -312,30 +291,7 @@ user-select: none; -webkit-user-select: none; -webkit-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; + app-region: drag; } /* 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; flex-shrink: 0; -webkit-app-region: no-drag; + app-region: no-drag; color: var(--text-muted); } @@ -409,6 +366,7 @@ padding: 0; min-width: 0; -webkit-app-region: no-drag; + app-region: no-drag; } .titlebar-search-input::placeholder { @@ -430,6 +388,7 @@ flex-shrink: 0; transition: background-color 150ms ease, color 150ms ease; -webkit-app-region: no-drag; + app-region: no-drag; } .titlebar-search-clear:hover { @@ -444,6 +403,7 @@ align-items: stretch; height: 100%; -webkit-app-region: no-drag; + app-region: no-drag; flex-shrink: 0; } @@ -492,6 +452,7 @@ cursor: pointer; transition: background-color 150ms ease, color 150ms ease; -webkit-app-region: no-drag; + app-region: no-drag; } /* --- Rectangular --- */ diff --git a/src/lib/components/views/SettingsView.svelte b/src/lib/components/views/SettingsView.svelte index 653d48dda..646b3d115 100644 --- a/src/lib/components/views/SettingsView.svelte +++ b/src/lib/components/views/SettingsView.svelte @@ -99,11 +99,12 @@ } from '$lib/stores/consoleLogStore'; import { subscribe as subscribeTitleBar, - getMode, - setMode, + getHideTitleBar, + setHideTitleBar, + getUseSystemTitleBar, + setUseSystemTitleBar, getShowWindowControls, - setShowWindowControls, - type TitlebarMode + setShowWindowControls } from '$lib/stores/titleBarStore'; import { subscribe as subscribeWindowChrome, @@ -1084,43 +1085,11 @@ let windowTitleTemplate = $state(DEFAULT_WINDOW_TITLE_TEMPLATE); // Title bar settings - const initialTitlebarMode: TitlebarMode = getMode(); - let titlebarMode = $state(initialTitlebarMode); - // 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( - initialTitlebarMode === 'plasma' ? ['qbz', 'system', 'plasma', 'hidden'] : ['qbz', 'system', 'hidden'] - ); - let isSandboxed = $state(false); + let hideTitleBar = $state(getHideTitleBar()); + let useSystemTitleBar = $state(getUseSystemTitleBar()); let matchSystemWindowChromeState = $state(getMatchSystemWindowChrome()); 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). // `null` until the first detect call returns. `isKlassy=true` means a // genuine Klassy install was detected; `desktop` starting with "plasma" @@ -1817,7 +1786,8 @@ // Subscribe to title bar state changes const unsubscribeTitleBar = subscribeTitleBar(() => { - titlebarMode = getMode(); + hideTitleBar = getHideTitleBar(); + useSystemTitleBar = getUseSystemTitleBar(); windowControlsVisible = getShowWindowControls(); }); const unsubscribeWindowChrome = subscribeWindowChrome(() => { @@ -4836,70 +4806,49 @@ /> {/if} - - {#if platform === 'macos'} + + {#if platform !== 'macos'} +
+
+ {$t('settings.appearance.useSystemTitleBar')} + {$t('settings.appearance.useSystemTitleBarDesc')} +
+ setUseSystemTitleBar(v)} /> +
{$t('settings.appearance.hideTitleBar')} {$t('settings.appearance.hideTitleBarDesc')}
- setMode(v ? 'hidden' : 'qbz')} - /> + setHideTitleBar(v)} disabled={useSystemTitleBar} />
- {/if} - {#if platform !== 'macos'}
-
- {$t('settings.titlebar.mode.label')} -
- getModeLabel(m))} - onchange={(label) => { - const next = getModeFromLabel(label); - if (next) setMode(next); - }} - /> -
- {#if titlebarMode === 'plasma' && isSandboxed} -

{$t('settings.titlebar.sandboxCaveat')}

- {/if} -
{$t('settings.appearance.matchSystemChrome')} {$t('settings.appearance.matchSystemChromeDesc')}
- - { - setMatchSystemWindowChrome(v); - showToast($t('settings.appearance.matchSystemChromeRestart'), 'info'); - }} - disabled={!isQbzMode} - /> - + { + setMatchSystemWindowChrome(v); + showToast($t('settings.appearance.matchSystemChromeRestart'), 'info'); + }} + disabled={hideTitleBar || useSystemTitleBar} + />
{/if} - -
+ + {#if platform !== 'macos'} +
{$t('settings.appearance.searchInTitleBar')} {$t('settings.appearance.searchInTitleBarDesc')}
- - setSearchBarLocation(v ? 'titlebar' : 'sidebar')} - disabled={!supportsTitlebarFeatures} - /> - + setSearchBarLocation(v ? 'titlebar' : 'sidebar')} + disabled={hideTitleBar || useSystemTitleBar} + />
@@ -4907,57 +4856,47 @@ {$t('settings.appearance.navInTitleBarDesc')}
-
+
{$t('nav.home')} - - setDiscoverInTitlebar(v)} - disabled={!supportsTitlebarFeatures} - /> - + setDiscoverInTitlebar(v)} + disabled={hideTitleBar || useSystemTitleBar} + />
-
+
{$t('nav.favorites')} - - setFavoritesInTitlebar(v)} - disabled={!supportsTitlebarFeatures} - /> - + setFavoritesInTitlebar(v)} + disabled={hideTitleBar || useSystemTitleBar} + />
-
+
{$t('library.title')} - - setLibraryInTitlebar(v)} - disabled={!supportsTitlebarFeatures} - /> - + setLibraryInTitlebar(v)} + disabled={hideTitleBar || useSystemTitleBar} + />
-
+
{$t('nav.myQbz')} - - setMyQbzInTitlebar(v)} - disabled={!supportsTitlebarFeatures} - /> - + setMyQbzInTitlebar(v)} + disabled={hideTitleBar || useSystemTitleBar} + />
-
+
{$t('nav.purchases')} - - setPurchasesInTitlebar(v)} - disabled={!supportsTitlebarFeatures} - /> - + setPurchasesInTitlebar(v)} + disabled={hideTitleBar || useSystemTitleBar} + />
- {#if titlebarNavAnyEnabled && supportsTitlebarFeatures} + {#if titlebarNavAnyEnabled && !hideTitleBar && !useSystemTitleBar}
{$t('settings.appearance.navInTitleBarPosition')} @@ -4980,11 +4919,7 @@ />
{/if} - - {#if platform !== 'macos'} -
+
{$t('settings.appearance.windowControlsPosition')} {$t('settings.appearance.windowControlsPositionDesc')} @@ -4995,7 +4930,7 @@ onchange={handleWcPositionChange} />
-
+
{$t('settings.appearance.windowControlsStyle')} {$t('settings.appearance.windowControlsStyleDesc')} @@ -5006,7 +4941,7 @@ onchange={handleWcShapeChange} />
-
+
{$t('settings.appearance.windowControlsSize')} {$t('settings.appearance.windowControlsSizeDesc')} @@ -5017,7 +4952,7 @@ onchange={handleWcSizeChange} />
-
+
{$t('settings.appearance.windowControlsColorPreset')} {$t('settings.appearance.windowControlsColorPresetDesc')} @@ -5029,7 +4964,7 @@ />
{#if wcConfig.preset === 'custom'} -
+
{$t('settings.appearance.windowControlsCustomColors')} {#each WC_BUTTONS as btn}
@@ -5057,18 +4992,16 @@ {/each}
{/if} -
+
{$t('settings.appearance.showWindowControls')} {$t('settings.appearance.showWindowControlsDesc')}
- - setShowWindowControls(v)} - disabled={!isQbzMode} - /> - + setShowWindowControls(v)} + disabled={hideTitleBar || useSystemTitleBar} + />
{/if}
@@ -7951,14 +7884,6 @@ flatpak override --user --filesystem=/home/USUARIO/Música com.blitzfc.qbz 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 */ .wc-custom-panel { margin: 0 0 8px 0; diff --git a/src/lib/i18n/locales/de.json b/src/lib/i18n/locales/de.json index d953fd58d..d4b686d8f 100644 --- a/src/lib/i18n/locales/de.json +++ b/src/lib/i18n/locales/de.json @@ -2456,26 +2456,6 @@ "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": { "title": "Bibliothek", "fetchArtistImages": "Qobuz™-Künstlerbilder abrufen", diff --git a/src/lib/i18n/locales/en.json b/src/lib/i18n/locales/en.json index 525c63fcf..d425fe204 100644 --- a/src/lib/i18n/locales/en.json +++ b/src/lib/i18n/locales/en.json @@ -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": { "title": "Library", "fetchArtistImages": "Fetch Qobuz™ Artist Images", diff --git a/src/lib/i18n/locales/es.json b/src/lib/i18n/locales/es.json index e6cbd7203..f12dccd6e 100644 --- a/src/lib/i18n/locales/es.json +++ b/src/lib/i18n/locales/es.json @@ -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": { "title": "Biblioteca", "fetchArtistImages": "Obtener imágenes de artistas de Qobuz™", diff --git a/src/lib/i18n/locales/fr.json b/src/lib/i18n/locales/fr.json index 3df42640d..9d0947c26 100644 --- a/src/lib/i18n/locales/fr.json +++ b/src/lib/i18n/locales/fr.json @@ -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": { "title": "Bibliothèque", "fetchArtistImages": "Récupérer les images d'artistes Qobuz™", diff --git a/src/lib/i18n/locales/pt.json b/src/lib/i18n/locales/pt.json index b4896df44..5403c66bb 100644 --- a/src/lib/i18n/locales/pt.json +++ b/src/lib/i18n/locales/pt.json @@ -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": { "title": "Biblioteca", "fetchArtistImages": "Buscar Imagens de Artistas do Qobuz™", diff --git a/src/lib/services/miniplayerService.ts b/src/lib/services/miniplayerService.ts index 80ef75e65..e27799b17 100644 --- a/src/lib/services/miniplayerService.ts +++ b/src/lib/services/miniplayerService.ts @@ -17,7 +17,7 @@ import { setMiniPlayerSurface, type MiniPlayerSurface } from '$lib/stores/uiStore'; -import { getMode } from '$lib/stores/titleBarStore'; +import { getUseSystemTitleBar } from '$lib/stores/titleBarStore'; import { getUserItem } from '$lib/utils/userStorage'; const MINI_PLAYER_LABEL = 'miniplayer'; @@ -93,10 +93,7 @@ async function getOrCreateMiniPlayerWindow(): Promise { minWidth: 340, minHeight: 57, resizable: true, - decorations: (() => { - const mode = getMode(); - return mode === 'system' || mode === 'plasma'; - })(), + decorations: getUseSystemTitleBar(), alwaysOnTop: miniState.alwaysOnTop, transparent: true, focus: true diff --git a/src/lib/stores/searchBarLocationStore.ts b/src/lib/stores/searchBarLocationStore.ts index 782984ffd..c7b170e98 100644 --- a/src/lib/stores/searchBarLocationStore.ts +++ b/src/lib/stores/searchBarLocationStore.ts @@ -74,17 +74,3 @@ export function setSearchBarLocation(value: SearchBarLocation): void { export function toggleSearchBarLocation(): void { 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(); -} diff --git a/src/lib/stores/titleBarStore.ts b/src/lib/stores/titleBarStore.ts index 2b7bb6d06..ca7101937 100644 --- a/src/lib/stores/titleBarStore.ts +++ b/src/lib/stores/titleBarStore.ts @@ -1,211 +1,178 @@ -/** - * Title Bar Settings Store - * - * Manages titlebar mode (single 4-value enum) plus the orthogonal - * `showWindowControls` preference. - * - * Modes: - * - 'qbz': default — full custom titlebar with drag/resize/dblclick/controls - * - '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 { getSearchBarLocation } from './searchBarLocationStore'; -import { isTitlebarNavEnabled } from './titlebarNavStore'; - -export type TitlebarMode = 'qbz' | 'system' | 'plasma' | 'hidden'; - -const STORAGE_KEY_MODE = 'qbz-titlebar-mode'; -const STORAGE_KEY_WINDOW_CONTROLS = 'qbz-show-window-controls'; -const LEGACY_KEY_HIDE = 'qbz-hide-titlebar'; -const LEGACY_KEY_SYSTEM = 'qbz-use-system-titlebar'; - -let mode: TitlebarMode = 'qbz'; -let showWindowControls = true; - -const listeners = new Set<() => void>(); - -function notifyListeners(): void { - for (const listener of listeners) { - listener(); - } -} - -function migrateLegacyKeys(): TitlebarMode | null { - // Returns the migrated mode, or null if no migration was needed. - const hide = localStorage.getItem(LEGACY_KEY_HIDE); - const system = localStorage.getItem(LEGACY_KEY_SYSTEM); - if (hide === null && system === null) return null; - - let migrated: TitlebarMode = 'qbz'; - if (hide === 'true') { - migrated = 'hidden'; - } else if (system === 'true') { - migrated = 'system'; - } - return migrated; -} - -export function initTitleBarStore(): void { - if (skipIfRemote()) return; - try { - const savedMode = localStorage.getItem(STORAGE_KEY_MODE); - if (savedMode === 'qbz' || savedMode === 'system' || savedMode === 'plasma' || savedMode === 'hidden') { - mode = savedMode; - } else { - // No persisted mode — try migrating from legacy keys. - const migrated = migrateLegacyKeys(); - 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) => { - console.error('[TitleBarStore] Failed to persist migrated mode to backend:', e); - }); - } - } - - const savedControls = localStorage.getItem(STORAGE_KEY_WINDOW_CONTROLS); - if (savedControls !== null) { - showWindowControls = savedControls !== 'false'; - } - } catch (e) { - console.error('[TitleBarStore] Failed to initialize:', e); - } -} - -export function subscribe(listener: () => void): () => void { - listeners.add(listener); - listener(); - return () => listeners.delete(listener); -} - -export function getMode(): TitlebarMode { - return mode; -} - -/** - * Whether the custom TitleBar.svelte component should mount. - * - 'qbz' → true (full variant). macOS always falls here: mode selection - * is hidden in settings on macOS, so it stays at the 'qbz' default and - * the strip mounts with native traffic lights overlaid on its left zone - * (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 - * for plasma, GTK CSD / WM SSD for system). - * - 'hidden' → false. - */ -export function shouldShowTitleBar(): boolean { - if (mode === 'hidden') return false; - 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. - return getSearchBarLocation() === 'titlebar' || isTitlebarNavEnabled(); -} - -/** - * 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. - */ -export function getTitleBarVariant(): 'full' | 'stripped' { - return mode === 'qbz' ? 'full' : 'stripped'; -} - -export function getTitleBarHeight(): number { - if (!shouldShowTitleBar()) return 0; - return mode === 'qbz' ? 44 : 42; -} - -export function getShowWindowControls(): boolean { - return showWindowControls; -} - -export function setShowWindowControls(value: boolean): void { - showWindowControls = value; - try { - localStorage.setItem(STORAGE_KEY_WINDOW_CONTROLS, String(value)); - } catch (e) { - console.error('[TitleBarStore] Failed to save window controls setting:', e); - } - notifyListeners(); -} - -/** - * Effective `showWindowControls` after applying mode rules. - * In 'plasma' mode, KWin draws controls in its SSD, so the custom - * controls are forced off regardless of the user pref. - */ -export function getEffectiveShowWindowControls(): boolean { - // macOS draws native traffic lights via TitleBarStyle::Overlay, so the - // custom controls would visually collide with them. - if (platform === 'macos') return false; - if (mode !== 'qbz') return false; - return showWindowControls; -} - -/** - * Set the titlebar mode. Persists to localStorage and backend, then - * restarts the app when the mode change crosses the decoration boundary - * (mounting/unmounting decorations requires window re-creation). - */ -export async function setMode(value: TitlebarMode): Promise { - if (skipIfRemote()) return; - - const previousWantsDecorations = - mode === 'system' || mode === 'plasma'; - const newWantsDecorations = - value === 'system' || value === 'plasma'; - - // Push to backend FIRST. If it fails, leave local state untouched. - try { - await invoke('v2_set_titlebar_mode', { mode: value }); - } catch (e) { - console.error('[TitleBarStore] Failed to push mode to backend:', e); - return; - } - - mode = value; - try { - localStorage.setItem(STORAGE_KEY_MODE, value); - } 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() - }; -} +/** + * Title Bar Settings Store + * + * Manages title bar visibility settings with localStorage persistence. + * + * Settings: + * - hideTitleBar: Remove title bar completely for tiling WM users (default: false) + * - useSystemTitleBar: Use OS native window decorations instead of custom title bar (default: false) + */ + +import { invoke } from '@tauri-apps/api/core'; +import { skipIfRemote } from '$lib/services/commandRouter'; +import { platform } from '$lib/utils/platform'; + +const STORAGE_KEY_HIDE = 'qbz-hide-titlebar'; +const STORAGE_KEY_SYSTEM = 'qbz-use-system-titlebar'; +const STORAGE_KEY_WINDOW_CONTROLS = 'qbz-show-window-controls'; + +// State +let hideTitleBar = false; +let useSystemTitleBar = false; +let showWindowControls = true; + +// Listeners +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; + try { + const savedHide = localStorage.getItem(STORAGE_KEY_HIDE); + if (savedHide !== null) { + hideTitleBar = savedHide === 'true'; + } + + const savedSystem = localStorage.getItem(STORAGE_KEY_SYSTEM); + if (savedSystem !== null) { + useSystemTitleBar = savedSystem === 'true'; + } + + const savedControls = localStorage.getItem(STORAGE_KEY_WINDOW_CONTROLS); + if (savedControls !== null) { + showWindowControls = savedControls !== 'false'; + } + // Sync localStorage value to Rust backend so it's available at next + // startup (before window creation). Handles migration from the + // localStorage-only era and keeps both stores in sync. + // Always sync, not just when true - otherwise false values never propagate. + invoke('v2_set_use_system_titlebar', { value: useSystemTitleBar }).catch((e) => { + console.error('[TitleBarStore] Failed to sync system titlebar to backend:', e); + }); + } catch (e) { + console.error('[TitleBarStore] Failed to initialize:', e); + } +} + +/** + * Subscribe to title bar state changes + */ +export function subscribe(listener: () => void): () => void { + listeners.add(listener); + listener(); + return () => listeners.delete(listener); +} + +/** + * Get current hide setting + */ +export function getHideTitleBar(): boolean { + return hideTitleBar; +} + +/** + * Get current system title bar setting + */ +export function getUseSystemTitleBar(): boolean { + return useSystemTitleBar; +} + +/** + * Determine if the custom title bar should be visible + * Hidden when either system title bar is active or hide mode is on + */ +export function shouldShowTitleBar(): boolean { + // macOS always uses native decorations — no custom title bar + if (platform === 'macos') return false; + return !hideTitleBar && !useSystemTitleBar; +} + +/** + * Get the title bar height for layout calculations + * Returns 0 if title bar is hidden or system title bar is active, 40 otherwise + */ +export function getTitleBarHeight(): number { + if (platform === 'macos') return 0; + return (hideTitleBar || useSystemTitleBar) ? 0 : 40; +} + +/** + * Get whether to show window control buttons (minimize/maximize/close) + */ +export function getShowWindowControls(): boolean { + return showWindowControls; +} + +/** + * Set whether to show window control buttons + */ +export function setShowWindowControls(value: boolean): void { + showWindowControls = value; + try { + localStorage.setItem(STORAGE_KEY_WINDOW_CONTROLS, String(value)); + } catch (e) { + console.error('[TitleBarStore] Failed to save window controls setting:', e); + } + notifyListeners(); +} + +/** + * Set whether to hide title bar completely + */ +export function setHideTitleBar(value: boolean): void { + hideTitleBar = value; + try { + localStorage.setItem(STORAGE_KEY_HIDE, String(value)); + } catch (e) { + console.error('[TitleBarStore] Failed to save hide titlebar setting:', e); + } + notifyListeners(); +} + +/** + * Set whether to use system (OS native) title bar. + * Persists to both localStorage and Rust backend, then restarts the app. + * Restart is required because the window must be created with the correct + * decoration state — runtime toggling doesn't work reliably on Linux/Wayland. + */ +export async function setUseSystemTitleBar(value: boolean): Promise { + if (skipIfRemote()) return; + useSystemTitleBar = value; + try { + localStorage.setItem(STORAGE_KEY_SYSTEM, String(value)); + } catch (e) { + console.error('[TitleBarStore] Failed to save system titlebar setting:', e); + } + try { + await invoke('v2_set_use_system_titlebar', { value }); + await invoke('v2_restart_app'); + } catch (e) { + console.error('[TitleBarStore] Failed to apply system titlebar change:', e); + } +} + +export interface TitleBarState { + hideTitleBar: boolean; + useSystemTitleBar: boolean; + showTitleBar: boolean; + showWindowControls: boolean; + titleBarHeight: number; +} + +export function getTitleBarState(): TitleBarState { + return { + hideTitleBar, + useSystemTitleBar, + showTitleBar: shouldShowTitleBar(), + showWindowControls, + titleBarHeight: getTitleBarHeight() + }; +} diff --git a/src/lib/stores/titlebarNavStore.ts b/src/lib/stores/titlebarNavStore.ts index af3cba60a..9e0717e0f 100644 --- a/src/lib/stores/titlebarNavStore.ts +++ b/src/lib/stores/titlebarNavStore.ts @@ -172,16 +172,3 @@ export function setTitlebarNavConfig(newConfig: Partial): voi persist(); 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(); -} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 25b723aef..13db2c1dc 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -123,7 +123,7 @@ import { subscribe as subscribeSearchBarLocation, initSearchBarLocation, - getEffectiveSearchBarLocation + getSearchBarLocation } from '$lib/stores/searchBarLocationStore'; // Window controls customization store @@ -551,8 +551,8 @@ let windowTransparent = $state(getWindowIsTransparent()); let showWindowControls = $state(getShowWindowControls()); - // Search Bar Location State (effective: respects mode, falls back to sidebar in system/hidden) - let searchBarLocationPref = $state(getEffectiveSearchBarLocation()); + // Search Bar Location State + let searchBarLocationPref = $state(getSearchBarLocation()); let titlebarSearchQuery = $state(getSearchQuery()); // Window Controls State @@ -4840,9 +4840,6 @@ const unsubscribeTitleBar = subscribeTitleBar(() => { showTitleBar = shouldShowTitleBar(); 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(); }); @@ -4877,11 +4874,7 @@ // Initialize and subscribe to search bar location initSearchBarLocation(); const unsubscribeSearchBarLocation = subscribeSearchBarLocation(() => { - searchBarLocationPref = getEffectiveSearchBarLocation(); - // 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(); + searchBarLocationPref = getSearchBarLocation(); }); // Initialize and subscribe to window controls customization @@ -4902,10 +4895,6 @@ tbNavLibrary = isLibraryInTitlebar(); tbNavMyQbz = isMyQbzInTitlebar(); 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