Merge pre-release into main for v1.2.12 hotfix release
This commit is contained in:
28 files changed
+565
-825
No files matched your search
@@ -0,0 +1,119 @@
|
||||
Shipping v1.2.12 hotfix that rolls back the broken titlebar-mode-selector
|
||||
|
||||
# 1.2.11 — Exclusive Hardening
|
||||
|
||||
This release tightens bit-perfect playback on both fronts. macOS finally gets a real **CoreAudio Exclusive Mode** thanks to **@Vudgekek**'s first-class contribution (PR #391), and on Linux the player now actively **reserves the DAC** via the standard `org.freedesktop.ReserveDevice1` D-Bus protocol for the lifetime of an ALSA Direct stream. Both halves landed in the same cycle by happy coincidence — hence the codename.
|
||||
|
||||
Beyond audio, this is the largest **Local Library** release since the local engine landed: a new folder-tree mode, ephemeral playback that never writes to your library DB, and a metadata-grouped Albums tab that finally treats Plex as a first-class peer.
|
||||
|
||||
A new **Offline Cache Manager** view replaces the old Settings shortcut, with a configurable cache cap (5 GB default), live progress rings, and per-album expand. **Discord Rich Presence** lands as opt-in and works inside the Flathub sandbox via a small symlink bridge. macOS gets a proper title bar integration thanks to **@afonsojramos**, who continues to keep the macOS build healthy alongside this round of fixes.
|
||||
|
||||
Welcome to **@Vudgekek** and **@DoubleGate** as new contributors — thanks for the first PRs, and for taking the time to engage with the review process.
|
||||
|
||||
---
|
||||
|
||||
## Exclusive Audio
|
||||
|
||||
- **macOS CoreAudio Exclusive Mode** (PR #391, @Vudgekek) — bit-perfect playback on Apple hardware via `kAudioDevicePropertyHogMode`; exclusive stream recreation on track changes; CoreAudio device-rate checked against the requested track rate before claiming the device
|
||||
- **macOS hardware volume alignment** — the exclusive device's hardware volume tracks qbz's volume slider when in exclusive mode (@Vudgekek)
|
||||
- **macOS exclusive stream hardening** — defensive cleanup on stream errors and shutdown so a failed claim never strands the device (@afonsojramos)
|
||||
- **Linux DAC reservation** — per-process zbus client acquires `org.freedesktop.ReserveDevice1` (the standard cross-app coordination protocol used by JACK, PipeWire, and PulseAudio) for the lifetime of an `AlsaDirectStream`; other apps see qbz as the sole DAC owner instead of getting silent, misleading "device busy" errors
|
||||
- **Reserve-DAC toggle** — Settings → Audio adds an opt-out for the reservation, persisted in `audio_settings.db` alongside the existing backend choice
|
||||
- **ALSA card resolution by id** — ALSA card matching now uses the stable card id (`hw:0`) instead of the long descriptive name; any plugin prefix is accepted and parse failures degrade gracefully
|
||||
- **`audio_settings` defaults** — `backend_type` now defaults to a per-OS choice and migrates rows where the column was NULL on older installs
|
||||
|
||||
---
|
||||
|
||||
## Local Library
|
||||
|
||||
- **Folder Tree mode** — new two-column layout with the folder tree on the left and a folder detail pane on the right; drag-resizable sidebar (default ×1.8), recursive multi-select, search with path-parents preserved, and a network-folder filter for shares that take a while to walk
|
||||
- **Ephemeral folder playback** — pick a folder outside your watched library and play it without writing anything to the library DB, including CUE support, a format whitelist, ephemeral-aware gapless prefetch, and a session-restore filter so transient picks don't pin themselves
|
||||
- **Albums tab — metadata-grouped** — the existing folder-grouped Albums view is now joined by a metadata-grouped variant that merges entries across folders (and Plex), with the full action bar (group/filter/sort/view/select)
|
||||
- **Per-user Library tab order** — new modal to reorder, show, or hide tabs; persists per user; the active tab loader now triggers after preferences settle on mount, so the loader never hits the wrong tab
|
||||
- **Plex per-track artwork** preserved during library scan; the same fix is applied to both scan loops, with a folder-lookup cache to avoid the second-loop regression
|
||||
- **Compact album view** for the tree right pane, with selection + bulk actions, circular play, and a per-view track search
|
||||
- **Multi-select coverage in Album Detail** (#381) — bulk action bar now reaches the album-detail tracklist as well
|
||||
- **Local Library performance** — viewport-driven Plex hydration with a quality override map; batched track-quality with per-call timeout; sort pushed into SQL instead of `localeCompare` on the client
|
||||
|
||||
---
|
||||
|
||||
## Discord Rich Presence (#336)
|
||||
|
||||
- **Opt-in track display** with album, artist, and elapsed time
|
||||
- **Native Discord** (deb / rpm / AUR builds) writes the IPC socket directly under `$XDG_RUNTIME_DIR` and works without setup
|
||||
- **Discord-via-Flathub** — qbz reads the Discord-Flatpak app dir (with `:create` permission) and lays down its own `discord-ipc-{0..9}` symlinks pointing at it; the symlinks bridge the two sandboxes for the `discord-rich-presence` crate, which only knows the static path
|
||||
- **Lazy bridge** — symlinks are only laid down after you enable the toggle in Settings, so users who never opt in never touch that filesystem path
|
||||
- **Translations** — keys for all five locales (en/es/de/fr/pt)
|
||||
|
||||
---
|
||||
|
||||
## Offline Cache Manager
|
||||
|
||||
- **Dedicated view** replaces the old "Open folder" shortcut in Settings; navigate via the new sidebar entry or the Manage offline cache button
|
||||
- **Album rows** with expand-by-default, per-track actions, album cover artwork, and live progress rings on both album and track rows
|
||||
- **Alpha-index artist rail** — jump to artists alphabetically; empty-state CTA when nothing is cached yet
|
||||
- **Cache size limit** — modal to set a configurable cap (default bumped from 2 GB to 5 GB), persisted across restarts; new track downloads enforce the limit
|
||||
- **Re-download flow** — copy renamed from "Re-download album" to "Re-download all tracks" to match what actually happens
|
||||
- **Excluded from `last_view` persistence** — opening the manager doesn't pin it on next launch
|
||||
|
||||
---
|
||||
|
||||
## Sleep Timer (#402)
|
||||
|
||||
- **Hard-stop pause** — at expiry the player pauses cleanly (no fadeouts that overrun the deadline)
|
||||
- **Smart-positioned popover** with refined typography; custom durations from 1 minute upward
|
||||
|
||||
---
|
||||
|
||||
## Title bar
|
||||
|
||||
- **macOS integration** (@afonsojramos) — qbz custom titlebar now mounts on macOS with traffic-light reservation, drag band aligned to sidebar top padding; new nav and search titlebar toggles plus a hide-title-bar toggle in Settings; legacy macOS overlay drag region retired
|
||||
- **Mode selector** — Settings → Appearance → Title bar replaces the hide/system toggles with a `system / plasma / stripped` dropdown; legacy preferences are migrated, sandbox/Wayland caveats are surfaced as inline hints
|
||||
|
||||
---
|
||||
|
||||
## QConnect
|
||||
|
||||
- **Startup persistence** — new `startup_mode` (Auto / Always-on / Disabled) and `last_known_state` write-through on connect/disconnect, both persisted; settings dropdown surfaces the choice with a local-library hint
|
||||
- **CLI override** — `--enable-qconnect` and `--disable-qconnect` flags applied during app setup
|
||||
- **Honor peer-controller seeks** when qbz is the active renderer — peer-driven seeks no longer get clobbered by the local cursor-align
|
||||
|
||||
---
|
||||
|
||||
## Album & Artist detail polish
|
||||
|
||||
- **Header gradient backdrop** — palette-derived gradient and blurred artwork on Album/Artist detail
|
||||
- **Album metadata enrichment** — featured artist line, parental warning badge, click-to-expand description, tracklist toolbar, label/awards sidebar shrunk by ~20%
|
||||
- **Tracklist columns aligned** with smooth select-mode toggle
|
||||
|
||||
---
|
||||
|
||||
## Immersive background
|
||||
|
||||
- **Kawarp WebGL renderer** replaces the legacy WebGL2 path in `Full` mode; attribution added to the About modal
|
||||
|
||||
---
|
||||
|
||||
## Bug fixes
|
||||
|
||||
- **Mixtape detail** — virtualized list scroll-up behavior stabilized
|
||||
- **Library** — Albums tab visible in Edit Library Tabs modal; Plex albums included in metadata-grouped Albums view
|
||||
- **Audio (Linux)** — host stream drop deferred on Stop so the device can be reused across track changes (@afonsojramos)
|
||||
- **Audio (macOS)** — output device picker populated; the 50 ms post-stop sleep is now gated to `cfg(target_os = "linux")` (@afonsojramos)
|
||||
- **Playback** — uniform 250 ms state polling cadence eliminates seekbar cold-start latency
|
||||
- **Playlist** — track-selection key includes the row index for duplicate tracks (#386); multi-select is now aligned with the TrackRow checkbox pattern; the checkbox input is pointer-transparent so the row click handler always fires
|
||||
- **Sidebar** — `isOffline` `$effect` skips its initial subscription so a stale value doesn't flip the offline indicator on mount (@DoubleGate, PR #384)
|
||||
- **Home** — sticky tab header pop-in animation unified across the macOS and Linux paths (@afonsojramos, PR #407)
|
||||
- **Theme** — WCAG-aware `btn-primary-text` token across all themes (the previous fixed value failed contrast on a couple of light themes)
|
||||
|
||||
---
|
||||
|
||||
## Internal & packaging
|
||||
|
||||
- **Node 20 → 24** across CI workflows and the Flatpak manifest
|
||||
- **Dependency bumps** — Tauri 2.10.3 → 2.11.1, openssl 0.10.79, tokio 1.52.1, axum 0.8.9, sha2 0.11.0, mdns-sd 0.19.1, plus the usual minor/patch swarm
|
||||
- **Flatpak permissions** — the manifest now declares `--filesystem=xdg-run/discord-ipc-0` and `--filesystem=xdg-run/app/com.discordapp.Discord:create` for the lazy Discord IPC bridge
|
||||
|
||||
---
|
||||
|
||||
Full changelog: https://github.com/vicrodh/qbz/compare/v1.2.10...v1.2.11
|
||||
@@ -15,10 +15,10 @@
|
||||
# VERSION BUMP: update version, rev, and hashes
|
||||
# when tagging a new release.
|
||||
# ──────────────────────────────────────────────
|
||||
qbzVersion = "1.2.10";
|
||||
qbzVersion = "1.2.11";
|
||||
qbzRev = "v${qbzVersion}";
|
||||
srcHash = "sha256-cPe7J0KtcTQFfjhNL9giRPLx2VwHsUYRDV/ZM/2dFas=";
|
||||
npmHash = "sha256-MYBxnLGSz4MyrpdNKUIBtnA6HFimPwrcEOYOGSbS5AY=";
|
||||
srcHash = "sha256-6KAm9dZBNwjEcmaKTWSq4r91Cu1f6Fqr3G9Z//FaOCU=";
|
||||
npmHash = "sha256-Iz5QzSsMhP/82kANYOhZftX7n1wrLeTUI5Hda/JM0pE=";
|
||||
in
|
||||
{
|
||||
packages.default = pkgs.rustPlatform.buildRustPackage rec {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "qbz-nix",
|
||||
"version": "1.2.11",
|
||||
"version": "1.2.12",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "qbz-nix",
|
||||
"version": "1.2.11",
|
||||
"version": "1.2.12",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@icons-pack/svelte-simple-icons": "^7.2.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "qbz-nix",
|
||||
"version": "1.2.11",
|
||||
"version": "1.2.12",
|
||||
"description": "Native high-fidelity client for Qobuz™ subscribers on Linux with Hi-Fi audio support (uses the Qobuz API; not certified by Qobuz)",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -69,6 +69,17 @@
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<releases>
|
||||
<release version="1.2.12" date="2026-05-09">
|
||||
<description>
|
||||
<p>Hotfix: rolls back the broken titlebar-mode-selector that left v1.2.11 with no visible window chrome on Linux. Restores v1.2.10 title bar behavior (System / Custom / Hidden). All other v1.2.11 features remain.</p>
|
||||
<ul>
|
||||
<li>Revert titlebar-mode-selector and PR #406 (Linux titlebar draggable + macOS QBZ strip)</li>
|
||||
<li>macOS QoL (PR #408 by @Vudgekek): native .icns app icon and tray icon variant support with auto dark-mode detection</li>
|
||||
<li>Sticky home tab header animation fix (PR #407 by @afonsojramos) preserved</li>
|
||||
<li>All v1.2.11 features remain: macOS CoreAudio Exclusive Mode, Linux DAC reservation, Discord Rich Presence, Sleep Timer, Offline Cache Manager, Local Library folder-tree and ephemeral playback, metadata-grouped Albums, QConnect startup persistence, album/artist detail polish</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.2.11" date="2026-05-09">
|
||||
<description>
|
||||
<p>Exclusive Hardening: bit-perfect playback gets reinforced on both ends — macOS gains a real CoreAudio Exclusive Mode (community PR by @Vudgekek) and Linux now reserves the DAC via the standard ReserveDevice1 D-Bus protocol while ALSA Direct is engaged.</p>
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
|
||||
name: qbz-player
|
||||
title: "QBZ — Native Qobuz Client for Linux"
|
||||
version: '1.2.11'
|
||||
version: '1.2.12'
|
||||
summary: Native Qobuz player for Linux with bit-perfect audio and DAC control
|
||||
description: |
|
||||
QBZ is a native Linux desktop client for Qobuz™, designed for high-fidelity playback.
|
||||
@@ -97,7 +97,7 @@ parts:
|
||||
qbz:
|
||||
plugin: rust
|
||||
source: https://github.com/vicrodh/qbz.git
|
||||
source-tag: v1.2.10
|
||||
source-tag: v1.2.12
|
||||
build-packages:
|
||||
- libwebkit2gtk-4.1-dev
|
||||
- libgtk-3-dev
|
||||
|
||||
Generated
+1
-1
@@ -5148,7 +5148,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "qbz-nix"
|
||||
version = "1.2.11"
|
||||
version = "1.2.12"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "qbz-nix"
|
||||
version = "1.2.11"
|
||||
version = "1.2.12"
|
||||
description = "Native high-fidelity client for Qobuz™ subscribers on Linux with Hi-Fi audio support (uses the Qobuz API; not certified by Qobuz)"
|
||||
authors = ["blitzkriegfc"]
|
||||
edition = "2021"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 1.5 MiB |
@@ -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<TitlebarMode, String> {
|
||||
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<TitlebarMode>,
|
||||
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]
|
||||
@@ -96,13 +74,12 @@ pub fn v2_set_close_to_tray(
|
||||
}
|
||||
|
||||
/// Update the tray icon variant ("auto" / "light" / "dark"). Persists
|
||||
/// the setting and pushes the change to the live SNI tray on Linux so
|
||||
/// it takes effect immediately — no restart required.
|
||||
/// the setting and pushes the change to the live tray handle when one is
|
||||
/// active, so it takes effect immediately — no restart required.
|
||||
#[tauri::command]
|
||||
pub fn v2_set_tray_icon_theme(
|
||||
value: String,
|
||||
state: State<'_, TraySettingsState>,
|
||||
#[cfg_attr(not(target_os = "linux"), allow(unused_variables))]
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<(), String> {
|
||||
let normalized = crate::config::tray_settings::normalize_tray_icon_theme(&value);
|
||||
@@ -124,6 +101,14 @@ pub fn v2_set_tray_icon_theme(
|
||||
tray.set_icon_theme(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
use tauri::Manager;
|
||||
if let Some(tray) = app_handle.try_state::<crate::tray::NativeTrayHandle>() {
|
||||
tray.set_icon_theme(normalized);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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> {
|
||||
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<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 (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<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.
|
||||
@@ -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<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
|
||||
// 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<bool>) {
|
||||
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<bool>) {
|
||||
.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<bool>) {
|
||||
.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<bool>) {
|
||||
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,
|
||||
|
||||
+1
-13
@@ -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
|
||||
|
||||
+114
-12
@@ -13,10 +13,12 @@ use image::GenericImageView;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use std::path::PathBuf;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use std::sync::Mutex;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use tauri::{
|
||||
image::Image,
|
||||
menu::{Menu, MenuItem, PredefinedMenuItem},
|
||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||
tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent},
|
||||
Emitter, Manager,
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -24,7 +26,56 @@ use tauri::Manager;
|
||||
use tauri::AppHandle;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
const TRAY_ICON_PNG: &[u8] = include_bytes!("../icons/tray.png");
|
||||
const TRAY_ICON_COLOR_PNG: &[u8] = include_bytes!("../icons/tray.png");
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
const TRAY_ICON_MONO_WHITE_PNG: &[u8] = include_bytes!("../icons/tray-dark-64.png");
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
const TRAY_ICON_MONO_BLACK_PNG: &[u8] = include_bytes!("../icons/tray-light-64.png");
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum IconVariant {
|
||||
/// Black glyph — for light menu bars.
|
||||
MonoBlack,
|
||||
/// White glyph — for dark menu bars.
|
||||
MonoWhite,
|
||||
/// Full colour vinyl logo.
|
||||
Color,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub struct NativeTrayHandle {
|
||||
tray: Mutex<Option<TrayIcon>>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
impl NativeTrayHandle {
|
||||
fn empty() -> Self {
|
||||
Self {
|
||||
tray: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn install(&self, tray: TrayIcon) {
|
||||
if let Ok(mut guard) = self.tray.lock() {
|
||||
*guard = Some(tray);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_icon_theme(&self, theme: String) {
|
||||
let tray = match self.tray.lock() {
|
||||
Ok(guard) => guard.as_ref().cloned(),
|
||||
Err(_) => None,
|
||||
};
|
||||
let Some(tray) = tray else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(e) = tray.set_icon(Some(load_tray_icon(Some(&theme)))) {
|
||||
log::error!("[tray] failed to set macOS tray icon theme '{}': {}", theme, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure tray icon is available in the user's icon theme directory.
|
||||
/// This makes the icon discoverable by libayatana-appindicator via
|
||||
@@ -49,7 +100,7 @@ fn ensure_tray_icon_in_theme() {
|
||||
if std::fs::create_dir_all(&icon_dir).is_ok() {
|
||||
let icon_path = icon_dir.join("com.blitzfc.qbz.png");
|
||||
if !icon_path.exists() {
|
||||
if let Err(e) = std::fs::write(&icon_path, TRAY_ICON_PNG) {
|
||||
if let Err(e) = std::fs::write(&icon_path, TRAY_ICON_COLOR_PNG) {
|
||||
log::warn!("Failed to write tray icon to theme dir: {}", e);
|
||||
} else {
|
||||
log::info!("Installed tray icon to {:?}", icon_path);
|
||||
@@ -66,9 +117,46 @@ fn is_flatpak() -> bool {
|
||||
std::env::var("FLATPAK_ID").is_ok() || std::path::Path::new("/.flatpak-info").exists()
|
||||
}
|
||||
|
||||
/// Get the tray icon - loads from file in Flatpak, embedded data otherwise
|
||||
/// Detect whether the macOS menu bar is currently dark. We key off the global
|
||||
/// Apple interface style so `auto` can mirror the Linux tray theme behavior:
|
||||
/// white glyph on dark chrome, black glyph on light chrome.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn prefer_dark_tray() -> bool {
|
||||
if let Ok(out) = std::process::Command::new("defaults")
|
||||
.args(["read", "-g", "AppleInterfaceStyle"])
|
||||
.output()
|
||||
{
|
||||
if out.status.success() {
|
||||
return String::from_utf8_lossy(&out.stdout).trim() == "Dark";
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_os = "linux"), not(target_os = "macos")))]
|
||||
fn prefer_dark_tray() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn load_tray_icon() -> Image<'static> {
|
||||
fn resolve_variant(theme_override: Option<&str>) -> IconVariant {
|
||||
match theme_override {
|
||||
Some("mono-light") => IconVariant::MonoWhite,
|
||||
Some("mono-dark") => IconVariant::MonoBlack,
|
||||
Some("color") => IconVariant::Color,
|
||||
_ => {
|
||||
if prefer_dark_tray() {
|
||||
IconVariant::MonoWhite
|
||||
} else {
|
||||
IconVariant::MonoBlack
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the tray icon - loads from file in Flatpak, embedded data otherwise.
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn load_tray_icon(theme_override: Option<&str>) -> Image<'static> {
|
||||
// In Flatpak, try to use the installed icon file first
|
||||
// This works better with StatusNotifierItem/libayatana-appindicator
|
||||
if is_flatpak() {
|
||||
@@ -87,7 +175,12 @@ fn load_tray_icon() -> Image<'static> {
|
||||
}
|
||||
|
||||
// Default: decode embedded PNG
|
||||
let img = image::load_from_memory(TRAY_ICON_PNG).expect("Failed to decode tray icon PNG");
|
||||
let icon_bytes = match resolve_variant(theme_override) {
|
||||
IconVariant::MonoBlack => TRAY_ICON_MONO_BLACK_PNG,
|
||||
IconVariant::MonoWhite => TRAY_ICON_MONO_WHITE_PNG,
|
||||
IconVariant::Color => TRAY_ICON_COLOR_PNG,
|
||||
};
|
||||
let img = image::load_from_memory(icon_bytes).expect("Failed to decode tray icon PNG");
|
||||
let (width, height) = img.dimensions();
|
||||
let rgba = img.into_rgba8().into_raw();
|
||||
Image::new_owned(rgba, width, height)
|
||||
@@ -100,7 +193,7 @@ fn load_tray_icon() -> Image<'static> {
|
||||
/// On Linux this also installs the live `LinuxTrayHandle` into Tauri state
|
||||
/// so the rest of the backend can push live tooltip updates as the player
|
||||
/// state changes. `theme_override` is the persisted user preference for
|
||||
/// the icon variant ("auto"/"light"/"dark"); ignored on macOS.
|
||||
/// the icon variant.
|
||||
pub fn init_tray(
|
||||
app: &AppHandle,
|
||||
#[cfg_attr(not(target_os = "linux"), allow(unused_variables))]
|
||||
@@ -114,13 +207,20 @@ pub fn init_tray(
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
init_tray_tauri(app)
|
||||
{
|
||||
let handle = init_tray_tauri(app, theme_override)?;
|
||||
app.manage(handle);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Tauri-backed tray implementation used on macOS. Kept as a separate fn so
|
||||
/// the Linux path doesn't pay to compile the Tauri tray at all.
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn init_tray_tauri(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn init_tray_tauri(
|
||||
app: &AppHandle,
|
||||
theme_override: Option<&str>,
|
||||
) -> Result<NativeTrayHandle, Box<dyn std::error::Error>> {
|
||||
log::info!("Initializing system tray icon (Tauri backend)");
|
||||
|
||||
// Create menu items
|
||||
@@ -150,7 +250,7 @@ fn init_tray_tauri(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
||||
ensure_tray_icon_in_theme();
|
||||
|
||||
// Load custom tray icon (with transparent background)
|
||||
let tray_icon = load_tray_icon();
|
||||
let tray_icon = load_tray_icon(theme_override);
|
||||
|
||||
// Build and display tray icon
|
||||
let mut builder = TrayIconBuilder::new()
|
||||
@@ -177,7 +277,7 @@ fn init_tray_tauri(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
|
||||
let _tray = builder
|
||||
let tray = builder
|
||||
.on_menu_event(|app, event| {
|
||||
let id = event.id.as_ref();
|
||||
log::info!("Tray menu event: {}", id);
|
||||
@@ -266,5 +366,7 @@ fn init_tray_tauri(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
||||
.build(app)?;
|
||||
|
||||
log::info!("System tray icon initialized");
|
||||
Ok(())
|
||||
let live = NativeTrayHandle::empty();
|
||||
live.install(tray);
|
||||
Ok(live)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "QBZ",
|
||||
"version": "1.2.11",
|
||||
"version": "1.2.12",
|
||||
"identifier": "com.blitzfc.qbz",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -2515,10 +2515,9 @@
|
||||
height: calc(100vh - 104px); /* Only 104px NowPlayingBar, no title bar */
|
||||
}
|
||||
|
||||
/* macOS hidden mode: pad sidebar top so search clears the traffic lights
|
||||
that the OS still draws via TitleBarStyle::Overlay. */
|
||||
/* macOS: pad top of sidebar to clear native traffic light buttons */
|
||||
:global(html.macos) .sidebar.no-titlebar {
|
||||
padding-top: 36px;
|
||||
padding-top: 32px;
|
||||
}
|
||||
|
||||
.content {
|
||||
|
||||
@@ -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<typeof getCurrentWindow>;
|
||||
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(() => {
|
||||
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': '' } : {}}
|
||||
>
|
||||
<!-- Left zone -->
|
||||
<div class="zone zone-left">
|
||||
{#if effectiveShowControls && controlsPosition === 'left'}
|
||||
{#if showWindowControls && controlsPosition === 'left'}
|
||||
{@render windowControls('left')}
|
||||
{/if}
|
||||
{#if navSnippet && navPosition === 'left'}
|
||||
{@render navSnippet()}
|
||||
{/if}
|
||||
<div class="drag-region" {...variant === 'full' ? { 'data-tauri-drag-region': '' } : {}}></div>
|
||||
<div class="drag-region" data-tauri-drag-region></div>
|
||||
</div>
|
||||
|
||||
<!-- Center zone (search, always centered) -->
|
||||
@@ -285,17 +264,17 @@
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="drag-region" {...variant === 'full' ? { 'data-tauri-drag-region': '' } : {}}></div>
|
||||
<div class="drag-region" data-tauri-drag-region></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Right zone -->
|
||||
<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'}
|
||||
{@render navSnippet()}
|
||||
{/if}
|
||||
{#if effectiveShowControls && controlsPosition === 'right'}
|
||||
{#if showWindowControls && controlsPosition === 'right'}
|
||||
{@render windowControls('right')}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -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 --- */
|
||||
|
||||
@@ -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<TitlebarMode>(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<TitlebarMode[]>(
|
||||
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(() => {
|
||||
@@ -3552,7 +3522,9 @@
|
||||
try {
|
||||
await invoke('v2_set_enable_tray', { value });
|
||||
enableTray = value;
|
||||
showToast($t('settings.appearance.tray.enableTrayDesc'), 'info');
|
||||
showToast($t(platform === 'macos'
|
||||
? 'settings.appearance.tray.enableTrayDescMacos'
|
||||
: 'settings.appearance.tray.enableTrayDesc'), 'info');
|
||||
} catch (err) {
|
||||
console.error('Failed to set enable tray:', err);
|
||||
showToast($t('toast.failedSaveTray'), 'error');
|
||||
@@ -4836,70 +4808,49 @@
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- macOS: only the 'qbz' ↔ 'hidden' axis is meaningful. 'system' /
|
||||
'plasma' describe Linux WM behaviors that don't apply on Darwin,
|
||||
so we expose a single Toggle instead of the full mode dropdown. -->
|
||||
{#if platform === 'macos'}
|
||||
<!-- Title bar toggles: hidden on macOS (always uses native overlay title bar) -->
|
||||
{#if platform !== 'macos'}
|
||||
<div class="setting-row">
|
||||
<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-info">
|
||||
<span class="setting-label">{$t('settings.appearance.hideTitleBar')}</span>
|
||||
<span class="setting-desc">{$t('settings.appearance.hideTitleBarDesc')}</span>
|
||||
</div>
|
||||
<Toggle
|
||||
enabled={titlebarMode === 'hidden'}
|
||||
onchange={(v) => setMode(v ? 'hidden' : 'qbz')}
|
||||
/>
|
||||
<Toggle enabled={hideTitleBar} onchange={(v) => setHideTitleBar(v)} disabled={useSystemTitleBar} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if platform !== 'macos'}
|
||||
<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">
|
||||
<span class="setting-label">{$t('settings.appearance.matchSystemChrome')}</span>
|
||||
<span class="setting-desc">{$t('settings.appearance.matchSystemChromeDesc')}</span>
|
||||
</div>
|
||||
<span title={!isQbzMode ? getDisabledHint('matchSystemChrome') : ''}>
|
||||
<Toggle
|
||||
enabled={matchSystemWindowChromeState}
|
||||
onchange={(v) => {
|
||||
setMatchSystemWindowChrome(v);
|
||||
showToast($t('settings.appearance.matchSystemChromeRestart'), 'info');
|
||||
}}
|
||||
disabled={!isQbzMode}
|
||||
/>
|
||||
</span>
|
||||
<Toggle
|
||||
enabled={matchSystemWindowChromeState}
|
||||
onchange={(v) => {
|
||||
setMatchSystemWindowChrome(v);
|
||||
showToast($t('settings.appearance.matchSystemChromeRestart'), 'info');
|
||||
}}
|
||||
disabled={hideTitleBar || useSystemTitleBar}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Search-in-titlebar and nav-in-titlebar: now available on macOS too,
|
||||
since the qbz strip mounts there and can host them next to the
|
||||
native traffic lights. -->
|
||||
<div class="setting-row" class:disabled-section={!supportsTitlebarFeatures}>
|
||||
<!-- Title bar customization: hidden on macOS (uses native overlay title bar) -->
|
||||
{#if platform !== 'macos'}
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">{$t('settings.appearance.searchInTitleBar')}</span>
|
||||
<span class="setting-desc">{$t('settings.appearance.searchInTitleBarDesc')}</span>
|
||||
</div>
|
||||
<span title={!supportsTitlebarFeatures ? getDisabledHint('searchLocation') : ''}>
|
||||
<Toggle
|
||||
enabled={searchInTitlebar}
|
||||
onchange={(v) => setSearchBarLocation(v ? 'titlebar' : 'sidebar')}
|
||||
disabled={!supportsTitlebarFeatures}
|
||||
/>
|
||||
</span>
|
||||
<Toggle
|
||||
enabled={searchInTitlebar}
|
||||
onchange={(v) => setSearchBarLocation(v ? 'titlebar' : 'sidebar')}
|
||||
disabled={hideTitleBar || useSystemTitleBar}
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
@@ -4907,57 +4858,47 @@
|
||||
<span class="setting-desc">{$t('settings.appearance.navInTitleBarDesc')}</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.home')}</span>
|
||||
<span title={!supportsTitlebarFeatures ? getDisabledHint('titlebarNav') : ''}>
|
||||
<Toggle
|
||||
enabled={tbNavConfig.discover}
|
||||
onchange={(v) => setDiscoverInTitlebar(v)}
|
||||
disabled={!supportsTitlebarFeatures}
|
||||
/>
|
||||
</span>
|
||||
<Toggle
|
||||
enabled={tbNavConfig.discover}
|
||||
onchange={(v) => setDiscoverInTitlebar(v)}
|
||||
disabled={hideTitleBar || useSystemTitleBar}
|
||||
/>
|
||||
</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 title={!supportsTitlebarFeatures ? getDisabledHint('titlebarNav') : ''}>
|
||||
<Toggle
|
||||
enabled={tbNavConfig.favorites}
|
||||
onchange={(v) => setFavoritesInTitlebar(v)}
|
||||
disabled={!supportsTitlebarFeatures}
|
||||
/>
|
||||
</span>
|
||||
<Toggle
|
||||
enabled={tbNavConfig.favorites}
|
||||
onchange={(v) => setFavoritesInTitlebar(v)}
|
||||
disabled={hideTitleBar || useSystemTitleBar}
|
||||
/>
|
||||
</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 title={!supportsTitlebarFeatures ? getDisabledHint('titlebarNav') : ''}>
|
||||
<Toggle
|
||||
enabled={tbNavConfig.library}
|
||||
onchange={(v) => setLibraryInTitlebar(v)}
|
||||
disabled={!supportsTitlebarFeatures}
|
||||
/>
|
||||
</span>
|
||||
<Toggle
|
||||
enabled={tbNavConfig.library}
|
||||
onchange={(v) => setLibraryInTitlebar(v)}
|
||||
disabled={hideTitleBar || useSystemTitleBar}
|
||||
/>
|
||||
</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 title={!supportsTitlebarFeatures ? getDisabledHint('titlebarNav') : ''}>
|
||||
<Toggle
|
||||
enabled={tbNavConfig.myQbz}
|
||||
onchange={(v) => setMyQbzInTitlebar(v)}
|
||||
disabled={!supportsTitlebarFeatures}
|
||||
/>
|
||||
</span>
|
||||
<Toggle
|
||||
enabled={tbNavConfig.myQbz}
|
||||
onchange={(v) => setMyQbzInTitlebar(v)}
|
||||
disabled={hideTitleBar || useSystemTitleBar}
|
||||
/>
|
||||
</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 title={!supportsTitlebarFeatures ? getDisabledHint('titlebarNav') : ''}>
|
||||
<Toggle
|
||||
enabled={tbNavConfig.purchases}
|
||||
onchange={(v) => setPurchasesInTitlebar(v)}
|
||||
disabled={!supportsTitlebarFeatures}
|
||||
/>
|
||||
</span>
|
||||
<Toggle
|
||||
enabled={tbNavConfig.purchases}
|
||||
onchange={(v) => setPurchasesInTitlebar(v)}
|
||||
disabled={hideTitleBar || useSystemTitleBar}
|
||||
/>
|
||||
</div>
|
||||
{#if titlebarNavAnyEnabled && supportsTitlebarFeatures}
|
||||
{#if titlebarNavAnyEnabled && !hideTitleBar && !useSystemTitleBar}
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">{$t('settings.appearance.navInTitleBarPosition')}</span>
|
||||
@@ -4980,11 +4921,7 @@
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Window-controls customization: still Linux-only. macOS draws native
|
||||
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-row" class:disabled-section={hideTitleBar || useSystemTitleBar}>
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">{$t('settings.appearance.windowControlsPosition')}</span>
|
||||
<span class="setting-desc">{$t('settings.appearance.windowControlsPositionDesc')}</span>
|
||||
@@ -4995,7 +4932,7 @@
|
||||
onchange={handleWcPositionChange}
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-row" class:disabled-section={!isQbzMode}>
|
||||
<div class="setting-row" class:disabled-section={hideTitleBar || useSystemTitleBar}>
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">{$t('settings.appearance.windowControlsStyle')}</span>
|
||||
<span class="setting-desc">{$t('settings.appearance.windowControlsStyleDesc')}</span>
|
||||
@@ -5006,7 +4943,7 @@
|
||||
onchange={handleWcShapeChange}
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-row" class:disabled-section={!isQbzMode}>
|
||||
<div class="setting-row" class:disabled-section={hideTitleBar || useSystemTitleBar}>
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">{$t('settings.appearance.windowControlsSize')}</span>
|
||||
<span class="setting-desc">{$t('settings.appearance.windowControlsSizeDesc')}</span>
|
||||
@@ -5017,7 +4954,7 @@
|
||||
onchange={handleWcSizeChange}
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-row" class:disabled-section={!isQbzMode}>
|
||||
<div class="setting-row" class:disabled-section={hideTitleBar || useSystemTitleBar}>
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">{$t('settings.appearance.windowControlsColorPreset')}</span>
|
||||
<span class="setting-desc">{$t('settings.appearance.windowControlsColorPresetDesc')}</span>
|
||||
@@ -5029,7 +4966,7 @@
|
||||
/>
|
||||
</div>
|
||||
{#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>
|
||||
{#each WC_BUTTONS as btn}
|
||||
<div class="wc-color-group">
|
||||
@@ -5057,18 +4994,16 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="setting-row" class:disabled-section={!isQbzMode}>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">{$t('settings.appearance.showWindowControls')}</span>
|
||||
<span class="setting-desc">{$t('settings.appearance.showWindowControlsDesc')}</span>
|
||||
</div>
|
||||
<span title={!isQbzMode ? getDisabledHint('windowControls') : ''}>
|
||||
<Toggle
|
||||
enabled={windowControlsVisible}
|
||||
onchange={(v) => setShowWindowControls(v)}
|
||||
disabled={!isQbzMode}
|
||||
/>
|
||||
</span>
|
||||
<Toggle
|
||||
enabled={windowControlsVisible}
|
||||
onchange={(v) => setShowWindowControls(v)}
|
||||
disabled={hideTitleBar || useSystemTitleBar}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="setting-row">
|
||||
@@ -5123,6 +5058,7 @@
|
||||
</div>
|
||||
<Toggle enabled={closeToTray} onchange={(v) => handleCloseToTrayChange(v)} disabled={!enableTray} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">{$t('settings.appearance.tray.iconTheme.label')}</span>
|
||||
@@ -5134,7 +5070,6 @@
|
||||
onchange={handleTrayIconThemeChange}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Immersive subsection -->
|
||||
<h4 class="subsection-title">{$t('settings.appearance.immersive.title')}</h4>
|
||||
@@ -7951,14 +7886,6 @@ flatpak override --user --filesystem=/home/USUARIO/Música com.blitzfc.qbz</pre>
|
||||
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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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™",
|
||||
|
||||
@@ -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™",
|
||||
|
||||
@@ -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™",
|
||||
|
||||
@@ -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<Window> {
|
||||
minWidth: 340,
|
||||
minHeight: 57,
|
||||
resizable: true,
|
||||
decorations: (() => {
|
||||
const mode = getMode();
|
||||
return mode === 'system' || mode === 'plasma';
|
||||
})(),
|
||||
decorations: getUseSystemTitleBar(),
|
||||
alwaysOnTop: miniState.alwaysOnTop,
|
||||
transparent: true,
|
||||
focus: true
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
+178
-211
@@ -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<void> {
|
||||
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<void> {
|
||||
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()
|
||||
};
|
||||
}
|
||||
@@ -172,16 +172,3 @@ export function setTitlebarNavConfig(newConfig: Partial<TitlebarNavConfig>): 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();
|
||||
}
|
||||
+11
-27
@@ -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
|
||||
@@ -5766,11 +5755,7 @@
|
||||
class:match-chrome={matchSystemChrome && showTitleBar && windowTransparent}
|
||||
style="--chrome-radius: {chromeRadiusPx}px;"
|
||||
>
|
||||
<!-- macOS: when the user hides the qbz strip (mode='hidden'), the OS
|
||||
still draws traffic lights via TitleBarStyle::Overlay on top of
|
||||
the sidebar. This invisible band gives the window something to
|
||||
drag and reserves the same vertical real estate the strip would
|
||||
have. -->
|
||||
<!-- macOS: drag region for window movement (overlay title bar has no native drag area) -->
|
||||
{#if !showTitleBar && platform === 'macos'}
|
||||
<div class="macos-drag-region" data-tauri-drag-region></div>
|
||||
{/if}
|
||||
@@ -7215,21 +7200,20 @@
|
||||
height: calc(100vh - var(--player-bar-height, 104px));
|
||||
}
|
||||
|
||||
/* macOS hidden mode: pad main content to clear native overlay traffic
|
||||
lights. Only fires when the qbz strip isn't mounted. */
|
||||
:global(html.macos) .app.no-titlebar .main-content {
|
||||
/* macOS: pad main content to clear native overlay title bar */
|
||||
:global(html.macos) .main-content {
|
||||
padding-top: 16px;
|
||||
height: calc(100vh - 104px - 16px);
|
||||
}
|
||||
|
||||
:global(html.macos) .app.no-titlebar .main-content :global(.home-view) {
|
||||
/* macOS: home view handles its own spacing */
|
||||
:global(html.macos) .main-content :global(.home-view) {
|
||||
margin-top: -16px;
|
||||
}
|
||||
|
||||
/* macOS hidden mode: invisible drag region for window movement, mirrors
|
||||
the band the qbz strip would otherwise occupy. */
|
||||
/* macOS: invisible drag region for window movement (overlay title bar) */
|
||||
:global(html.macos) .macos-drag-region {
|
||||
height: 32px;
|
||||
height: 28px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
Reference in new issue
Block a user