From d49137475fe6cbd5db62f8df42cf1843c7de6d2d Mon Sep 17 00:00:00 2001 From: Psychotoxical Date: Tue, 7 Apr 2026 21:12:17 +0200 Subject: [PATCH] feat(titlebar): custom Linux title bar with now-playing display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - set_decorations(false) on Linux at startup via #[cfg(target_os = "linux")] - New TitleBar component: drag region + minimize/maximize/close buttons - Currently playing track (▶/⏸ + artist – title) shown in the center - app-shell grid gains a 32px titlebar row when data-titlebar is set - IS_LINUX utility constant via navigator.platform - macOS and Windows are completely unaffected Co-Authored-By: Claude Sonnet 4.6 --- src-tauri/src/lib.rs | 11 +++++ src/App.tsx | 6 ++- src/components/TitleBar.tsx | 55 ++++++++++++++++++++++ src/styles/layout.css | 92 +++++++++++++++++++++++++++++++++++++ src/utils/platform.ts | 2 + 5 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 src/components/TitleBar.tsx create mode 100644 src/utils/platform.ts diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2490b2c1..ae69b939 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -826,6 +826,17 @@ pub fn run() { })) .setup(|app| { + // ── Custom title bar on Linux ───────────────────────────────── + // Remove OS window decorations so the React TitleBar component + // takes over. macOS and Windows keep their native decorations. + #[cfg(target_os = "linux")] + { + use tauri::Manager; + if let Some(win) = app.get_webview_window("main") { + let _ = win.set_decorations(false); + } + } + // ── System tray ─────────────────────────────────────────────── // Always build on startup; the frontend calls toggle_tray_icon(false) // immediately after load if the user has disabled the tray icon. diff --git a/src/App.tsx b/src/App.tsx index c4b89f0e..26ddb851 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -51,6 +51,8 @@ import GenreDetail from './pages/GenreDetail'; import ExportPickerModal from './components/ExportPickerModal'; import ChangelogModal from './components/ChangelogModal'; import AppUpdater from './components/AppUpdater'; +import TitleBar from './components/TitleBar'; +import { IS_LINUX } from './utils/platform'; import { version } from '../package.json'; import { useConnectionStatus } from './hooks/useConnectionStatus'; import { useAuthStore } from './store/authStore'; @@ -248,16 +250,18 @@ function AppShell() { const isMobilePlayer = isMobile && location.pathname === '/now-playing'; return ( -
e.preventDefault()} > + {IS_LINUX && } {!isMobile && ( s.currentTrack); + const isPlaying = usePlayerStore(s => s.isPlaying); + + return ( +
+ Psysonic + +
+ {currentTrack && ( + <> + {isPlaying ? '▶' : '⏸'} + + {currentTrack.artist && `${currentTrack.artist} – `}{currentTrack.title} + + + )} +
+ +
+ + + +
+
+ ); +} diff --git a/src/styles/layout.css b/src/styles/layout.css index 245137d4..180152e5 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -27,6 +27,98 @@ background: var(--bg-app); } +/* ─── Custom title bar (Linux only — decorations: false) ─── */ +:root { + --titlebar-height: 32px; +} + +.app-shell[data-titlebar] { + grid-template-rows: var(--titlebar-height) 1fr var(--player-height); + grid-template-areas: + "titlebar titlebar titlebar" + "sidebar main queue" + "player player player"; +} + +.titlebar { + grid-area: titlebar; + display: flex; + align-items: center; + justify-content: space-between; + background: var(--bg-sidebar); + border-bottom: 1px solid var(--border-subtle); + padding: 0 6px 0 12px; + height: var(--titlebar-height); + user-select: none; +} + +.titlebar-title { + font-size: 12px; + font-weight: 600; + color: var(--text-muted); + letter-spacing: 0.02em; + pointer-events: none; + flex: 0 0 auto; +} + +.titlebar-track { + position: absolute; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: 6px; + max-width: 40%; + pointer-events: none; +} + +.titlebar-track-state { + font-size: 9px; + color: var(--accent); + flex-shrink: 0; +} + +.titlebar-track-text { + font-size: 12px; + color: var(--text-secondary); + max-width: 100%; +} + +.titlebar-controls { + display: flex; + gap: 2px; + flex: 0 0 auto; +} + +.titlebar-btn { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 24px; + border: none; + background: transparent; + color: var(--text-muted); + border-radius: var(--radius-sm); + cursor: pointer; + transition: background var(--transition-fast), color var(--transition-fast); +} + +.titlebar-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.titlebar-btn-close:hover { + background: var(--danger); + color: #fff; +} + +/* Resizer handles must start below the titlebar */ +.app-shell[data-titlebar] .resizer { + top: var(--titlebar-height); +} + /* ─── Resizer Handles ─── */ .resizer { position: absolute; diff --git a/src/utils/platform.ts b/src/utils/platform.ts new file mode 100644 index 00000000..91e1c5e2 --- /dev/null +++ b/src/utils/platform.ts @@ -0,0 +1,2 @@ +/** True when running on Linux (WebKitGTK). Used to show the custom title bar. */ +export const IS_LINUX = navigator.platform.toLowerCase().includes('linux');