fix: player bar CSS + Windows media keys HWND (v1.17.2)

- layout: overflow:hidden on .app-shell + min-height:0 on sidebar/main/queue
  prevents player bar from being pushed off-screen when window is resized
  below OS minHeight constraint (ignored by some Linux WMs)
- lib.rs: pass main window HWND to souvlaki PlatformConfig on Windows so
  SMTC hooks into the existing Win32 message loop instead of creating its
  own — fixes media keys on Windows without crashing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-03-26 18:15:25 +01:00
parent 65a828e3fa
commit b67c198227
6 changed files with 84 additions and 51 deletions
+9
View File
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.17.2] - 2026-03-26
### Fixed
- **Player bar disappears when window is resized small**: On Linux (and some Windows configurations), the window manager ignores the `minHeight` constraint, allowing the window to be dragged smaller than intended. The CSS grid's `1fr` row has an implicit `min-height: auto`, meaning it refuses to shrink below the min-content height of the sidebar/main/queue children — this pushed the total grid height beyond `100vh` and scrolled the player bar out of view. Fixed by adding `min-height: 0` to `.sidebar`, `.main-content`, and `.queue-panel`, and `overflow: hidden` to `.app-shell` as a safety net.
- **Media keys on Windows (SMTC)**: souvlaki's Windows backend requires a valid Win32 HWND to hook into the existing message loop rather than spinning up its own. Passing `hwnd: None` caused a crash on startup (v1.17.0). Now retrieves the main window's HWND via `app.get_webview_window("main").hwnd()` and passes it to `PlatformConfig`. Falls back to disabled gracefully if the HWND cannot be obtained.
---
## [1.17.1] - 2026-03-25 ## [1.17.1] - 2026-03-25
### Fixed ### Fixed
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "psysonic", "name": "psysonic",
"version": "1.17.1", "version": "1.17.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+1 -1
View File
@@ -1,6 +1,6 @@
# Maintainer: Psychotoxic <psychotoxic@gmx.de> # Maintainer: Psychotoxic <psychotoxic@gmx.de>
pkgname=psysonic pkgname=psysonic
pkgver=1.17.1 pkgver=1.17.2
pkgrel=1 pkgrel=1
pkgdesc="Desktop music player for Subsonic API-compatible servers (Navidrome, Gonic, etc.)" pkgdesc="Desktop music player for Subsonic API-compatible servers (Navidrome, Gonic, etc.)"
arch=('x86_64') arch=('x86_64')
+63 -48
View File
@@ -252,61 +252,76 @@ pub fn run() {
.build(app)?; .build(app)?;
// ── MPRIS2 / OS media controls via souvlaki ────────────────── // ── MPRIS2 / OS media controls via souvlaki ──────────────────
// Windows: souvlaki SMTC init requires a valid HWND and a running
// COM message loop, neither of which is available this early in
// setup(). Disabled on Windows until we can defer init post-window.
// mpris_set_metadata / mpris_set_playback no-op via the None branch.
#[cfg(target_os = "windows")]
app.manage(MprisControls::new(None));
#[cfg(not(target_os = "windows"))]
{ {
use souvlaki::{MediaControlEvent, MediaControls, PlatformConfig}; use souvlaki::{MediaControlEvent, MediaControls, PlatformConfig};
// On Linux, souvlaki requires a live D-Bus session. // Collect pre-conditions and the platform-specific HWND.
#[cfg(target_os = "linux")] // Returns None early (with a log) on any unrecoverable condition
let dbus_ok = std::env::var("DBUS_SESSION_BUS_ADDRESS") // so app.manage() always executes exactly once at the bottom.
.map(|v| !v.is_empty()) let maybe_controls: Option<MediaControls> = (|| {
.unwrap_or(false); // Linux: requires a live D-Bus session.
#[cfg(not(target_os = "linux"))] #[cfg(target_os = "linux")]
let dbus_ok = true; {
let dbus_ok = std::env::var("DBUS_SESSION_BUS_ADDRESS")
if !dbus_ok { .map(|v| !v.is_empty())
eprintln!("[Psysonic] No D-Bus session — MPRIS media controls disabled"); .unwrap_or(false);
app.manage(MprisControls::new(None)); if !dbus_ok {
} else { eprintln!("[Psysonic] No D-Bus session — MPRIS media controls disabled");
return None;
let config = PlatformConfig {
dbus_name: "psysonic",
display_name: "Psysonic",
hwnd: None,
};
let maybe_controls = match MediaControls::new(config) {
Ok(mut controls) => {
let app_handle = app.handle().clone();
if let Err(e) = controls.attach(move |event: MediaControlEvent| {
let event_name = match event {
MediaControlEvent::Toggle => "media:play-pause",
MediaControlEvent::Play => "media:play-pause",
MediaControlEvent::Pause => "media:play-pause",
MediaControlEvent::Next => "media:next",
MediaControlEvent::Previous => "media:prev",
_ => return,
};
let _ = app_handle.emit(event_name, ());
}) {
eprintln!("[Psysonic] Failed to attach media controls: {e:?}");
} }
Some(controls)
} }
Err(e) => {
eprintln!("[Psysonic] Could not create media controls: {e:?}"); // Windows: souvlaki SMTC must hook into the existing Win32
None // message loop rather than spinning up its own. Pass the
// main window's HWND so it can do so. If we can't get one,
// skip init (no crash, just no media overlay).
#[cfg(target_os = "windows")]
let hwnd = {
use tauri::Manager;
let h = app.get_webview_window("main")
.and_then(|w| w.hwnd().ok())
.map(|h| h.0 as *mut std::ffi::c_void);
if h.is_none() {
eprintln!("[Psysonic] Could not get HWND — Windows media controls disabled");
return None;
}
h
};
#[cfg(not(target_os = "windows"))]
let hwnd: Option<*mut std::ffi::c_void> = None;
let config = PlatformConfig {
dbus_name: "psysonic",
display_name: "Psysonic",
hwnd,
};
match MediaControls::new(config) {
Ok(mut controls) => {
let app_handle = app.handle().clone();
if let Err(e) = controls.attach(move |event: MediaControlEvent| {
let event_name = match event {
MediaControlEvent::Toggle => "media:play-pause",
MediaControlEvent::Play => "media:play-pause",
MediaControlEvent::Pause => "media:play-pause",
MediaControlEvent::Next => "media:next",
MediaControlEvent::Previous => "media:prev",
_ => return,
};
let _ = app_handle.emit(event_name, ());
}) {
eprintln!("[Psysonic] Failed to attach media controls: {e:?}");
}
Some(controls)
}
Err(e) => {
eprintln!("[Psysonic] Could not create media controls: {e:?}");
None
}
} }
}; })();
app.manage(MprisControls::new(maybe_controls)); app.manage(MprisControls::new(maybe_controls));
} // end dbus_ok
} }
Ok(()) Ok(())
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Psysonic", "productName": "Psysonic",
"version": "1.17.1", "version": "1.17.2",
"identifier": "dev.psysonic.player", "identifier": "dev.psysonic.player",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",
+9
View File
@@ -9,6 +9,12 @@
"sidebar main queue" "sidebar main queue"
"player player player"; "player player player";
height: 100vh; height: 100vh;
/* overflow: hidden keeps the player bar pinned at the bottom even when the
window is dragged below the OS minHeight constraint (ignored on some
Linux WMs/compositors). Without this, the 1fr row's implicit auto
min-height can push the grid taller than 100vh, scrolling the player
bar out of view. */
overflow: hidden;
background: var(--bg-app); background: var(--bg-app);
} }
@@ -39,6 +45,7 @@
border-right: 1px solid var(--border-subtle); border-right: 1px solid var(--border-subtle);
position: relative; position: relative;
z-index: 2; z-index: 2;
min-height: 0; /* allow 1fr row to shrink freely */
} }
.sidebar-brand { .sidebar-brand {
@@ -301,6 +308,7 @@
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
min-width: 0; min-width: 0;
min-height: 0; /* allow 1fr row to shrink freely */
background: var(--bg-app); background: var(--bg-app);
z-index: 1; z-index: 1;
} }
@@ -635,6 +643,7 @@
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
min-width: 0; min-width: 0;
min-height: 0; /* allow 1fr row to shrink freely */
} }
.queue-header { .queue-header {