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
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
@@ -252,12 +254,14 @@ function AppShell() {
|
||||
className="app-shell"
|
||||
data-mobile={isMobile || undefined}
|
||||
data-mobile-player={isMobilePlayer || undefined}
|
||||
data-titlebar={IS_LINUX || 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 && <TitleBar />}
|
||||
{!isMobile && (
|
||||
<Sidebar
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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