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 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-07 21:47:29 +02:00
parent d49137475f
commit 45fa606ae1
7 changed files with 43 additions and 2 deletions
+9
View File
@@ -36,6 +36,14 @@ fn exit_app(app_handle: tauri::AppHandle) {
app_handle.exit(0); 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. /// Authenticate with Navidrome's own REST API and return a Bearer token.
async fn navidrome_token(server_url: &str, username: &str, password: &str) -> Result<String, String> { async fn navidrome_token(server_url: &str, username: &str, password: &str) -> Result<String, String> {
@@ -962,6 +970,7 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
greet, greet,
exit_app, exit_app,
set_window_decorations,
register_global_shortcut, register_global_shortcut,
unregister_global_shortcut, unregister_global_shortcut,
mpris_set_metadata, mpris_set_metadata,
+9 -2
View File
@@ -89,9 +89,16 @@ function AppShell() {
const isLoggedIn = useAuthStore(s => s.isLoggedIn); const isLoggedIn = useAuthStore(s => s.isLoggedIn);
const activeServerId = useAuthStore(s => s.activeServerId); const activeServerId = useAuthStore(s => s.activeServerId);
const setMusicFolders = useAuthStore(s => s.setMusicFolders); const setMusicFolders = useAuthStore(s => s.setMusicFolders);
const useCustomTitlebar = useAuthStore(s => s.useCustomTitlebar);
const offlineAlbums = useOfflineStore(s => s.albums); const offlineAlbums = useOfflineStore(s => s.albums);
const hasOfflineContent = Object.values(offlineAlbums).some(a => a.serverId === serverId); 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(() => { useEffect(() => {
if (!isLoggedIn || !activeServerId) return; if (!isLoggedIn || !activeServerId) return;
let cancelled = false; let cancelled = false;
@@ -254,14 +261,14 @@ function AppShell() {
className="app-shell" className="app-shell"
data-mobile={isMobile || undefined} data-mobile={isMobile || undefined}
data-mobile-player={isMobilePlayer || undefined} data-mobile-player={isMobilePlayer || undefined}
data-titlebar={IS_LINUX || undefined} data-titlebar={(IS_LINUX && useCustomTitlebar) || undefined}
style={{ style={{
'--sidebar-width': isMobile ? '0px' : (isSidebarCollapsed ? '72px' : 'clamp(200px, 15vw, 220px)'), '--sidebar-width': isMobile ? '0px' : (isSidebarCollapsed ? '72px' : 'clamp(200px, 15vw, 220px)'),
'--queue-width': isMobile ? '0px' : (isQueueVisible ? `${queueWidth}px` : '0px') '--queue-width': isMobile ? '0px' : (isQueueVisible ? `${queueWidth}px` : '0px')
} as React.CSSProperties} } as React.CSSProperties}
onContextMenu={e => e.preventDefault()} onContextMenu={e => e.preventDefault()}
> >
{IS_LINUX && <TitleBar />} {IS_LINUX && useCustomTitlebar && <TitleBar />}
{!isMobile && ( {!isMobile && (
<Sidebar <Sidebar
isCollapsed={isSidebarCollapsed} isCollapsed={isSidebarCollapsed}
+2
View File
@@ -437,6 +437,8 @@ export const deTranslation = {
minimizeToTrayDesc: 'Beim Schließen des Fensters läuft Psysonic weiter im System-Tray statt zu beenden.', minimizeToTrayDesc: 'Beim Schließen des Fensters läuft Psysonic weiter im System-Tray statt zu beenden.',
discordRichPresence: 'Discord Rich Presence', discordRichPresence: 'Discord Rich Presence',
discordRichPresenceDesc: 'Zeigt den aktuell gespielten Titel im Discord-Profil an. Discord muss dafür geöffnet sein.', discordRichPresenceDesc: 'Zeigt den aktuell gespielten Titel im Discord-Profil an. Discord muss dafür geöffnet sein.',
useCustomTitlebar: 'Eigene Titelleiste',
useCustomTitlebarDesc: 'Ersetzt die System-Titelleiste durch eine eingebaute, die zum App-Theme passt. Deaktivieren, um die native GNOME/GTK-Titelleiste zu verwenden.',
discordAppleCovers: 'Cover über Apple Music für Discord laden', discordAppleCovers: 'Cover über Apple Music für Discord laden',
discordAppleCoversDesc: 'Sendet Künstler- und Albumname an die Apple-Such-API, um Cover für dein Discord-Profil zu finden. Standardmäßig aus Datenschutzgründen deaktiviert.', discordAppleCoversDesc: 'Sendet Künstler- und Albumname an die Apple-Such-API, um Cover für dein Discord-Profil zu finden. Standardmäßig aus Datenschutzgründen deaktiviert.',
nowPlayingEnabled: 'Im Livefenster anzeigen', nowPlayingEnabled: 'Im Livefenster anzeigen',
+2
View File
@@ -438,6 +438,8 @@ export const enTranslation = {
minimizeToTrayDesc: 'When closing the window, keep Psysonic running in the system tray instead of quitting.', minimizeToTrayDesc: 'When closing the window, keep Psysonic running in the system tray instead of quitting.',
discordRichPresence: 'Discord Rich Presence', discordRichPresence: 'Discord Rich Presence',
discordRichPresenceDesc: 'Show the currently playing track on your Discord profile. Requires Discord to be running.', discordRichPresenceDesc: 'Show the currently playing track on your Discord profile. Requires Discord to be running.',
useCustomTitlebar: 'Custom title bar',
useCustomTitlebarDesc: 'Replace the system title bar with a built-in one that matches the app theme. Disable to use the native GNOME/GTK title bar.',
discordAppleCovers: 'Fetch covers from Apple Music for Discord', discordAppleCovers: 'Fetch covers from Apple Music for Discord',
discordAppleCoversDesc: 'Sends the artist and album name to Apple\'s search API to find cover art for your Discord profile. Disabled by default for privacy.', discordAppleCoversDesc: 'Sends the artist and album name to Apple\'s search API to find cover art for your Discord profile. Disabled by default for privacy.',
nowPlayingEnabled: 'Show in Now Playing', nowPlayingEnabled: 'Show in Now Playing',
+1
View File
@@ -134,6 +134,7 @@ export default function Home() {
{isVisible('mostPlayed') && ( {isVisible('mostPlayed') && (
<AlbumRow <AlbumRow
title={t('home.mostPlayed')} title={t('home.mostPlayed')}
titleLink="/most-played"
albums={mostPlayed} albums={mostPlayed}
onLoadMore={() => loadMore('frequent', mostPlayed, setMostPlayed)} onLoadMore={() => loadMore('frequent', mostPlayed, setMostPlayed)}
moreText={t('home.loadMore')} moreText={t('home.loadMore')}
+16
View File
@@ -19,6 +19,7 @@ import LastfmIcon from '../components/LastfmIcon';
import CustomSelect from '../components/CustomSelect'; import CustomSelect from '../components/CustomSelect';
import ThemePicker from '../components/ThemePicker'; import ThemePicker from '../components/ThemePicker';
import { useAuthStore, ServerProfile } from '../store/authStore'; import { useAuthStore, ServerProfile } from '../store/authStore';
import { IS_LINUX } from '../utils/platform';
import { useThemeStore } from '../store/themeStore'; import { useThemeStore } from '../store/themeStore';
import { useFontStore, FontId } from '../store/fontStore'; import { useFontStore, FontId } from '../store/fontStore';
import { useKeybindingsStore, KeyAction, formatKeyCode, DEFAULT_BINDINGS } from '../store/keybindingsStore'; import { useKeybindingsStore, KeyAction, formatKeyCode, DEFAULT_BINDINGS } from '../store/keybindingsStore';
@@ -615,6 +616,21 @@ export default function Settings() {
<span className="toggle-track" /> <span className="toggle-track" />
</label> </label>
</div> </div>
{IS_LINUX && (
<>
<div className="settings-section-divider" />
<div className="settings-toggle-row">
<div>
<div style={{ fontWeight: 500 }}>{t('settings.useCustomTitlebar')}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.useCustomTitlebarDesc')}</div>
</div>
<label className="toggle-switch" aria-label={t('settings.useCustomTitlebar')}>
<input type="checkbox" checked={auth.useCustomTitlebar} onChange={e => auth.setUseCustomTitlebar(e.target.checked)} />
<span className="toggle-track" />
</label>
</div>
</>
)}
<div className="settings-section-divider" /> <div className="settings-section-divider" />
<div className="settings-toggle-row"> <div className="settings-toggle-row">
<div> <div>
+4
View File
@@ -42,6 +42,7 @@ interface AuthState {
minimizeToTray: boolean; minimizeToTray: boolean;
discordRichPresence: boolean; discordRichPresence: boolean;
enableAppleMusicCoversDiscord: boolean; enableAppleMusicCoversDiscord: boolean;
useCustomTitlebar: boolean;
nowPlayingEnabled: boolean; nowPlayingEnabled: boolean;
lyricsServerFirst: boolean; lyricsServerFirst: boolean;
showFullscreenLyrics: boolean; showFullscreenLyrics: boolean;
@@ -105,6 +106,7 @@ interface AuthState {
setMinimizeToTray: (v: boolean) => void; setMinimizeToTray: (v: boolean) => void;
setDiscordRichPresence: (v: boolean) => void; setDiscordRichPresence: (v: boolean) => void;
setEnableAppleMusicCoversDiscord: (v: boolean) => void; setEnableAppleMusicCoversDiscord: (v: boolean) => void;
setUseCustomTitlebar: (v: boolean) => void;
setNowPlayingEnabled: (v: boolean) => void; setNowPlayingEnabled: (v: boolean) => void;
setLyricsServerFirst: (v: boolean) => void; setLyricsServerFirst: (v: boolean) => void;
setShowFullscreenLyrics: (v: boolean) => void; setShowFullscreenLyrics: (v: boolean) => void;
@@ -156,6 +158,7 @@ export const useAuthStore = create<AuthState>()(
minimizeToTray: false, minimizeToTray: false,
discordRichPresence: false, discordRichPresence: false,
enableAppleMusicCoversDiscord: false, enableAppleMusicCoversDiscord: false,
useCustomTitlebar: true,
nowPlayingEnabled: false, nowPlayingEnabled: false,
lyricsServerFirst: true, lyricsServerFirst: true,
showFullscreenLyrics: true, showFullscreenLyrics: true,
@@ -240,6 +243,7 @@ export const useAuthStore = create<AuthState>()(
setMinimizeToTray: (v) => set({ minimizeToTray: v }), setMinimizeToTray: (v) => set({ minimizeToTray: v }),
setDiscordRichPresence: (v) => set({ discordRichPresence: v }), setDiscordRichPresence: (v) => set({ discordRichPresence: v }),
setEnableAppleMusicCoversDiscord: (v) => set({ enableAppleMusicCoversDiscord: v }), setEnableAppleMusicCoversDiscord: (v) => set({ enableAppleMusicCoversDiscord: v }),
setUseCustomTitlebar: (v) => set({ useCustomTitlebar: v }),
setNowPlayingEnabled: (v) => set({ nowPlayingEnabled: v }), setNowPlayingEnabled: (v) => set({ nowPlayingEnabled: v }),
setLyricsServerFirst: (v: boolean) => set({ lyricsServerFirst: v }), setLyricsServerFirst: (v: boolean) => set({ lyricsServerFirst: v }),
setShowFullscreenLyrics: (v: boolean) => set({ showFullscreenLyrics: v }), setShowFullscreenLyrics: (v: boolean) => set({ showFullscreenLyrics: v }),