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/),
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
### Fixed
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "psysonic",
"version": "1.17.1",
"version": "1.17.2",
"private": true,
"scripts": {
"dev": "vite",
+1 -1
View File
@@ -1,6 +1,6 @@
# Maintainer: Psychotoxic <psychotoxic@gmx.de>
pkgname=psysonic
pkgver=1.17.1
pkgver=1.17.2
pkgrel=1
pkgdesc="Desktop music player for Subsonic API-compatible servers (Navidrome, Gonic, etc.)"
arch=('x86_64')
+33 -18
View File
@@ -252,37 +252,51 @@ pub fn run() {
.build(app)?;
// ── 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};
// On Linux, souvlaki requires a live D-Bus session.
// Collect pre-conditions and the platform-specific HWND.
// Returns None early (with a log) on any unrecoverable condition
// so app.manage() always executes exactly once at the bottom.
let maybe_controls: Option<MediaControls> = (|| {
// Linux: requires a live D-Bus session.
#[cfg(target_os = "linux")]
{
let dbus_ok = std::env::var("DBUS_SESSION_BUS_ADDRESS")
.map(|v| !v.is_empty())
.unwrap_or(false);
#[cfg(not(target_os = "linux"))]
let dbus_ok = true;
if !dbus_ok {
eprintln!("[Psysonic] No D-Bus session — MPRIS media controls disabled");
app.manage(MprisControls::new(None));
} else {
return None;
}
}
// Windows: souvlaki SMTC must hook into the existing Win32
// 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: None,
hwnd,
};
let maybe_controls = match MediaControls::new(config) {
match MediaControls::new(config) {
Ok(mut controls) => {
let app_handle = app.handle().clone();
if let Err(e) = controls.attach(move |event: MediaControlEvent| {
@@ -304,9 +318,10 @@ pub fn run() {
eprintln!("[Psysonic] Could not create media controls: {e:?}");
None
}
};
}
})();
app.manage(MprisControls::new(maybe_controls));
} // end dbus_ok
}
Ok(())
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Psysonic",
"version": "1.17.1",
"version": "1.17.2",
"identifier": "dev.psysonic.player",
"build": {
"beforeDevCommand": "npm run dev",
+9
View File
@@ -9,6 +9,12 @@
"sidebar main queue"
"player player player";
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);
}
@@ -39,6 +45,7 @@
border-right: 1px solid var(--border-subtle);
position: relative;
z-index: 2;
min-height: 0; /* allow 1fr row to shrink freely */
}
.sidebar-brand {
@@ -301,6 +308,7 @@
flex-direction: column;
overflow: hidden;
min-width: 0;
min-height: 0; /* allow 1fr row to shrink freely */
background: var(--bg-app);
z-index: 1;
}
@@ -635,6 +643,7 @@
flex-direction: column;
overflow: hidden;
min-width: 0;
min-height: 0; /* allow 1fr row to shrink freely */
}
.queue-header {