From 45fa606ae1462579b147573f4b14ae6c6d5772a8 Mon Sep 17 00:00:00 2001 From: Psychotoxical Date: Tue, 7 Apr 2026 21:47:29 +0200 Subject: [PATCH] feat(titlebar): make custom title bar optional via Settings toggle - New authStore field useCustomTitlebar (default: true) - set_window_decorations Tauri command toggles native decorations at runtime - Settings toggle visible only on Linux (no restart required) - i18n keys added to EN + DE Co-Authored-By: Claude Sonnet 4.6 --- src-tauri/src/lib.rs | 9 +++++++++ src/App.tsx | 11 +++++++++-- src/locales/de.ts | 2 ++ src/locales/en.ts | 2 ++ src/pages/Home.tsx | 1 + src/pages/Settings.tsx | 16 ++++++++++++++++ src/store/authStore.ts | 4 ++++ 7 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ae69b939..5d29900e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -36,6 +36,14 @@ fn exit_app(app_handle: tauri::AppHandle) { app_handle.exit(0); } +/// Toggle native window decorations at runtime (Linux custom title bar opt-out). +#[tauri::command] +fn set_window_decorations(enabled: bool, app_handle: tauri::AppHandle) { + if let Some(win) = app_handle.get_webview_window("main") { + let _ = win.set_decorations(enabled); + } +} + /// Authenticate with Navidrome's own REST API and return a Bearer token. async fn navidrome_token(server_url: &str, username: &str, password: &str) -> Result { @@ -962,6 +970,7 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ greet, exit_app, + set_window_decorations, register_global_shortcut, unregister_global_shortcut, mpris_set_metadata, diff --git a/src/App.tsx b/src/App.tsx index 26ddb851..7ce99a77 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -89,9 +89,16 @@ function AppShell() { const isLoggedIn = useAuthStore(s => s.isLoggedIn); const activeServerId = useAuthStore(s => s.activeServerId); const setMusicFolders = useAuthStore(s => s.setMusicFolders); + const useCustomTitlebar = useAuthStore(s => s.useCustomTitlebar); const offlineAlbums = useOfflineStore(s => s.albums); const hasOfflineContent = Object.values(offlineAlbums).some(a => a.serverId === serverId); + // Sync custom titlebar preference with native decorations on Linux + useEffect(() => { + if (!IS_LINUX) return; + invoke('set_window_decorations', { enabled: !useCustomTitlebar }).catch(() => {}); + }, [useCustomTitlebar]); + useEffect(() => { if (!isLoggedIn || !activeServerId) return; let cancelled = false; @@ -254,14 +261,14 @@ function AppShell() { className="app-shell" data-mobile={isMobile || undefined} data-mobile-player={isMobilePlayer || undefined} - data-titlebar={IS_LINUX || undefined} + data-titlebar={(IS_LINUX && useCustomTitlebar) || undefined} style={{ '--sidebar-width': isMobile ? '0px' : (isSidebarCollapsed ? '72px' : 'clamp(200px, 15vw, 220px)'), '--queue-width': isMobile ? '0px' : (isQueueVisible ? `${queueWidth}px` : '0px') } as React.CSSProperties} onContextMenu={e => e.preventDefault()} > - {IS_LINUX && } + {IS_LINUX && useCustomTitlebar && } {!isMobile && ( loadMore('frequent', mostPlayed, setMostPlayed)} moreText={t('home.loadMore')} diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index a496a526..d6fc9bad 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -19,6 +19,7 @@ import LastfmIcon from '../components/LastfmIcon'; import CustomSelect from '../components/CustomSelect'; import ThemePicker from '../components/ThemePicker'; import { useAuthStore, ServerProfile } from '../store/authStore'; +import { IS_LINUX } from '../utils/platform'; import { useThemeStore } from '../store/themeStore'; import { useFontStore, FontId } from '../store/fontStore'; import { useKeybindingsStore, KeyAction, formatKeyCode, DEFAULT_BINDINGS } from '../store/keybindingsStore'; @@ -615,6 +616,21 @@ export default function Settings() { + {IS_LINUX && ( + <> +
+
+
+
{t('settings.useCustomTitlebar')}
+
{t('settings.useCustomTitlebarDesc')}
+
+ +
+ + )}
diff --git a/src/store/authStore.ts b/src/store/authStore.ts index c820890b..b1d9a9d8 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -42,6 +42,7 @@ interface AuthState { minimizeToTray: boolean; discordRichPresence: boolean; enableAppleMusicCoversDiscord: boolean; + useCustomTitlebar: boolean; nowPlayingEnabled: boolean; lyricsServerFirst: boolean; showFullscreenLyrics: boolean; @@ -105,6 +106,7 @@ interface AuthState { setMinimizeToTray: (v: boolean) => void; setDiscordRichPresence: (v: boolean) => void; setEnableAppleMusicCoversDiscord: (v: boolean) => void; + setUseCustomTitlebar: (v: boolean) => void; setNowPlayingEnabled: (v: boolean) => void; setLyricsServerFirst: (v: boolean) => void; setShowFullscreenLyrics: (v: boolean) => void; @@ -156,6 +158,7 @@ export const useAuthStore = create()( minimizeToTray: false, discordRichPresence: false, enableAppleMusicCoversDiscord: false, + useCustomTitlebar: true, nowPlayingEnabled: false, lyricsServerFirst: true, showFullscreenLyrics: true, @@ -240,6 +243,7 @@ export const useAuthStore = create()( setMinimizeToTray: (v) => set({ minimizeToTray: v }), setDiscordRichPresence: (v) => set({ discordRichPresence: v }), setEnableAppleMusicCoversDiscord: (v) => set({ enableAppleMusicCoversDiscord: v }), + setUseCustomTitlebar: (v) => set({ useCustomTitlebar: v }), setNowPlayingEnabled: (v) => set({ nowPlayingEnabled: v }), setLyricsServerFirst: (v: boolean) => set({ lyricsServerFirst: v }), setShowFullscreenLyrics: (v: boolean) => set({ showFullscreenLyrics: v }),