mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
feat(titlebar): custom Linux title bar with now-playing display (#titlebar)
Replaces the native GTK/GNOME title bar with a built-in one that matches the app theme on all Linux desktops. Tested on CachyOS + KDE Plasma and Fedora + GNOME. - set_decorations(false) on Linux at startup - TitleBar component: drag region, minimize/maximize/close, centered now-playing - Hides automatically in native fullscreen (F11) via onResized+isFullscreen - Optional: toggle in Settings → Verhalten (default: on) - macOS and Windows completely unaffected Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,8 @@
|
|||||||
"window-state:allow-restore-state",
|
"window-state:allow-restore-state",
|
||||||
"core:window:allow-set-title",
|
"core:window:allow-set-title",
|
||||||
"core:window:allow-close",
|
"core:window:allow-close",
|
||||||
|
"core:window:allow-minimize",
|
||||||
|
"core:window:allow-toggle-maximize",
|
||||||
"core:window:allow-hide",
|
"core:window:allow-hide",
|
||||||
"core:window:allow-show",
|
"core:window:allow-show",
|
||||||
"core:window:allow-set-fullscreen",
|
"core:window:allow-set-fullscreen",
|
||||||
|
|||||||
@@ -36,6 +36,19 @@ 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);
|
||||||
|
// Re-enabling native decorations on GTK causes the window manager to
|
||||||
|
// re-stack the window, which drops focus. Bring it back immediately.
|
||||||
|
if enabled {
|
||||||
|
let _ = win.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// 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> {
|
||||||
@@ -826,6 +839,17 @@ pub fn run() {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
.setup(|app| {
|
.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 ───────────────────────────────────────────────
|
// ── System tray ───────────────────────────────────────────────
|
||||||
// Always build on startup; the frontend calls toggle_tray_icon(false)
|
// Always build on startup; the frontend calls toggle_tray_icon(false)
|
||||||
// immediately after load if the user has disabled the tray icon.
|
// immediately after load if the user has disabled the tray icon.
|
||||||
@@ -951,6 +975,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,
|
||||||
|
|||||||
+22
@@ -51,6 +51,8 @@ import GenreDetail from './pages/GenreDetail';
|
|||||||
import ExportPickerModal from './components/ExportPickerModal';
|
import ExportPickerModal from './components/ExportPickerModal';
|
||||||
import ChangelogModal from './components/ChangelogModal';
|
import ChangelogModal from './components/ChangelogModal';
|
||||||
import AppUpdater from './components/AppUpdater';
|
import AppUpdater from './components/AppUpdater';
|
||||||
|
import TitleBar from './components/TitleBar';
|
||||||
|
import { IS_LINUX } from './utils/platform';
|
||||||
import { version } from '../package.json';
|
import { version } from '../package.json';
|
||||||
import { useConnectionStatus } from './hooks/useConnectionStatus';
|
import { useConnectionStatus } from './hooks/useConnectionStatus';
|
||||||
import { useAuthStore } from './store/authStore';
|
import { useAuthStore } from './store/authStore';
|
||||||
@@ -73,6 +75,17 @@ function RequireAuth({ children }: { children: React.ReactNode }) {
|
|||||||
function AppShell() {
|
function AppShell() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const [isWindowFullscreen, setIsWindowFullscreen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!IS_LINUX) return;
|
||||||
|
const win = getCurrentWindow();
|
||||||
|
let unlisten: (() => void) | undefined;
|
||||||
|
win.onResized(() => {
|
||||||
|
win.isFullscreen().then(setIsWindowFullscreen).catch(() => {});
|
||||||
|
}).then(u => { unlisten = u; });
|
||||||
|
return () => { unlisten?.(); };
|
||||||
|
}, []);
|
||||||
const isFullscreenOpen = usePlayerStore(s => s.isFullscreenOpen);
|
const isFullscreenOpen = usePlayerStore(s => s.isFullscreenOpen);
|
||||||
const toggleFullscreen = usePlayerStore(s => s.toggleFullscreen);
|
const toggleFullscreen = usePlayerStore(s => s.toggleFullscreen);
|
||||||
const isQueueVisible = usePlayerStore(s => s.isQueueVisible);
|
const isQueueVisible = usePlayerStore(s => s.isQueueVisible);
|
||||||
@@ -87,9 +100,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;
|
||||||
@@ -252,12 +272,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 && useCustomTitlebar && !isWindowFullscreen) || 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 && useCustomTitlebar && !isWindowFullscreen && <TitleBar />}
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<Sidebar
|
<Sidebar
|
||||||
isCollapsed={isSidebarCollapsed}
|
isCollapsed={isSidebarCollapsed}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||||
|
import { X, Minus, Square } from 'lucide-react';
|
||||||
|
import { usePlayerStore } from '../store/playerStore';
|
||||||
|
|
||||||
|
const win = getCurrentWindow();
|
||||||
|
|
||||||
|
export default function TitleBar() {
|
||||||
|
const currentTrack = usePlayerStore(s => s.currentTrack);
|
||||||
|
const isPlaying = usePlayerStore(s => s.isPlaying);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="titlebar" data-tauri-drag-region>
|
||||||
|
<span className="titlebar-title" data-tauri-drag-region>Psysonic</span>
|
||||||
|
|
||||||
|
<div className="titlebar-track" data-tauri-drag-region>
|
||||||
|
{currentTrack && (
|
||||||
|
<>
|
||||||
|
<span className="titlebar-track-state">{isPlaying ? '▶' : '⏸'}</span>
|
||||||
|
<span className="titlebar-track-text truncate">
|
||||||
|
{currentTrack.artist && `${currentTrack.artist} – `}{currentTrack.title}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="titlebar-controls">
|
||||||
|
<button
|
||||||
|
className="titlebar-btn titlebar-btn-minimize"
|
||||||
|
onClick={() => win.minimize()}
|
||||||
|
data-tooltip="Minimize"
|
||||||
|
data-tooltip-pos="bottom"
|
||||||
|
>
|
||||||
|
<Minus size={10} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="titlebar-btn titlebar-btn-maximize"
|
||||||
|
onClick={() => win.toggleMaximize()}
|
||||||
|
data-tooltip="Maximize"
|
||||||
|
data-tooltip-pos="bottom"
|
||||||
|
>
|
||||||
|
<Square size={9} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="titlebar-btn titlebar-btn-close"
|
||||||
|
onClick={() => win.close()}
|
||||||
|
data-tooltip="Close"
|
||||||
|
data-tooltip-pos="bottom"
|
||||||
|
>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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')}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
|||||||
@@ -27,6 +27,98 @@
|
|||||||
background: var(--bg-app);
|
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 Handles ─── */
|
||||||
.resizer {
|
.resizer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -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');
|
||||||
Reference in New Issue
Block a user