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:
16 files changed
+287
-782
No files matched your search
@@ -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]
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 --- */
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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™",
|
||||||
|
|||||||
@@ -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™",
|
||||||
|
|||||||
@@ -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™",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user