mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
fix(mini-player): unhang Windows by pre-creating the mini webview
Creating the second WebView2 webview lazily from the open_mini_player invoke handler reliably stalled Tauri's event loop on Windows — the mini opened blank white, neither main nor mini could be closed, and the user had to kill the process via Task Manager. The builder-then-minimize-main combo racing with WebView2's first paint seems to be the trigger. Fix: extract a build_mini_player_window helper and call it once in .setup() on Windows so the webview is created (hidden) in the startup context, not from an invoke command. open_mini_player is now a pure show/hide on all platforms. Windows also skips the main.minimize() call since that interacted with the hang; macOS + Linux keep the existing behaviour of minimising main when the mini opens. Other changes bundled in: - Switch Windows from the custom in-page titlebar back to native window decorations (the earlier decorations(false) move on Windows was part of the hang surface — safer to keep the OS chrome there, Linux still uses the custom bar). - Re-emit mini:ready on window focus so every open of the pre-created mini forces a fresh snapshot from main's bridge, even when the mount- time emit raced past the bridge during startup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+74
-48
@@ -2705,41 +2705,13 @@ fn default_mini_position(app: &tauri::AppHandle) -> Option<tauri::PhysicalPositi
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open (or toggle) the mini player window. Creates it on first call; on
|
/// Build the mini player webview window. Caller decides `visible` so the
|
||||||
/// subsequent calls, hides it if visible, shows + focuses it if hidden.
|
/// same code path serves both pre-creation (Windows, hidden at app start)
|
||||||
/// Opening the mini player minimizes the main window; hiding the mini player
|
/// and lazy creation (other platforms, shown on demand).
|
||||||
/// restores the main window.
|
fn build_mini_player_window(
|
||||||
#[tauri::command]
|
app: &tauri::AppHandle,
|
||||||
fn open_mini_player(app: tauri::AppHandle) -> Result<(), String> {
|
visible: bool,
|
||||||
if let Some(win) = app.get_webview_window("mini") {
|
) -> Result<tauri::WebviewWindow, String> {
|
||||||
let visible = win.is_visible().unwrap_or(false);
|
|
||||||
if visible {
|
|
||||||
win.hide().map_err(|e| e.to_string())?;
|
|
||||||
if let Some(main) = app.get_webview_window("main") {
|
|
||||||
let _ = main.unminimize();
|
|
||||||
let _ = main.show();
|
|
||||||
let _ = main.set_focus();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Re-applying the saved position after show() — many Linux WMs
|
|
||||||
// (Mutter, KWin) re-centre hidden windows when they're shown
|
|
||||||
// again, ignoring any earlier set_position. Mark the move as
|
|
||||||
// programmatic so the Moved-event handler doesn't echo the
|
|
||||||
// intermediate centre coords back to disk.
|
|
||||||
let target = read_mini_pos(&app);
|
|
||||||
mark_mini_pos_programmatic();
|
|
||||||
win.show().map_err(|e| e.to_string())?;
|
|
||||||
let _ = win.set_focus();
|
|
||||||
if let Some(p) = target {
|
|
||||||
let _ = win.set_position(tauri::PhysicalPosition::new(p.x, p.y));
|
|
||||||
}
|
|
||||||
if let Some(main) = app.get_webview_window("main") {
|
|
||||||
let _ = main.minimize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let use_always_on_top = {
|
let use_always_on_top = {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{ !is_tiling_wm() }
|
{ !is_tiling_wm() }
|
||||||
@@ -2750,9 +2722,9 @@ fn open_mini_player(app: tauri::AppHandle) -> Result<(), String> {
|
|||||||
// Resolve target position BEFORE building so the WM places the window
|
// Resolve target position BEFORE building so the WM places the window
|
||||||
// correctly from creation. Calling `set_position` after `build()` is
|
// correctly from creation. Calling `set_position` after `build()` is
|
||||||
// unreliable on several Linux WMs which re-centre hidden windows.
|
// unreliable on several Linux WMs which re-centre hidden windows.
|
||||||
let target_physical = read_mini_pos(&app)
|
let target_physical = read_mini_pos(app)
|
||||||
.map(|p| tauri::PhysicalPosition::new(p.x, p.y))
|
.map(|p| tauri::PhysicalPosition::new(p.x, p.y))
|
||||||
.or_else(|| default_mini_position(&app));
|
.or_else(|| default_mini_position(app));
|
||||||
let scale = app
|
let scale = app
|
||||||
.primary_monitor()
|
.primary_monitor()
|
||||||
.ok()
|
.ok()
|
||||||
@@ -2760,13 +2732,13 @@ fn open_mini_player(app: tauri::AppHandle) -> Result<(), String> {
|
|||||||
.map(|m| m.scale_factor())
|
.map(|m| m.scale_factor())
|
||||||
.unwrap_or(1.0);
|
.unwrap_or(1.0);
|
||||||
|
|
||||||
// macOS keeps the native titlebar (traffic lights + system look).
|
// macOS + Windows keep the native titlebar (traffic lights / caption
|
||||||
// Windows and Linux use a custom in-page titlebar so the mini fits a
|
// buttons + system look). Linux uses a custom in-page titlebar so the
|
||||||
// tighter visual style across all WMs (incl. tiling).
|
// mini fits a tighter visual style across all WMs (incl. tiling).
|
||||||
let use_decorations = cfg!(target_os = "macos");
|
let use_decorations = !cfg!(target_os = "linux");
|
||||||
|
|
||||||
let mut builder = tauri::WebviewWindowBuilder::new(
|
let mut builder = tauri::WebviewWindowBuilder::new(
|
||||||
&app,
|
app,
|
||||||
"mini",
|
"mini",
|
||||||
tauri::WebviewUrl::App("index.html".into()),
|
tauri::WebviewUrl::App("index.html".into()),
|
||||||
)
|
)
|
||||||
@@ -2776,7 +2748,8 @@ fn open_mini_player(app: tauri::AppHandle) -> Result<(), String> {
|
|||||||
.resizable(true)
|
.resizable(true)
|
||||||
.decorations(use_decorations)
|
.decorations(use_decorations)
|
||||||
.always_on_top(use_always_on_top)
|
.always_on_top(use_always_on_top)
|
||||||
.skip_taskbar(false);
|
.skip_taskbar(false)
|
||||||
|
.visible(visible);
|
||||||
|
|
||||||
if let Some(pos) = target_physical {
|
if let Some(pos) = target_physical {
|
||||||
builder = builder.position(pos.x as f64 / scale, pos.y as f64 / scale);
|
builder = builder.position(pos.x as f64 / scale, pos.y as f64 / scale);
|
||||||
@@ -2786,13 +2759,51 @@ fn open_mini_player(app: tauri::AppHandle) -> Result<(), String> {
|
|||||||
// fire stray Moved events with default coords during the first paint.
|
// fire stray Moved events with default coords during the first paint.
|
||||||
mark_mini_pos_programmatic();
|
mark_mini_pos_programmatic();
|
||||||
|
|
||||||
let win = builder
|
builder
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| format!("failed to build mini player window: {e}"))?;
|
.map_err(|e| format!("failed to build mini player window: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
let _ = win.set_focus();
|
/// Open (or toggle) the mini player window. On platforms where the window
|
||||||
if let Some(main) = app.get_webview_window("main") {
|
/// was pre-created at startup (Windows), this is a pure show/hide. On
|
||||||
let _ = main.minimize();
|
/// other platforms the window is created lazily on first call.
|
||||||
|
/// Opening the mini player minimizes the main window; hiding the mini
|
||||||
|
/// player restores the main window. Both steps are skipped on Windows
|
||||||
|
/// because creating + immediately minimizing main stalls WebView2's paint
|
||||||
|
/// pipeline and locks up the Tauri event loop.
|
||||||
|
#[tauri::command]
|
||||||
|
fn open_mini_player(app: tauri::AppHandle) -> Result<(), String> {
|
||||||
|
let win = match app.get_webview_window("mini") {
|
||||||
|
Some(w) => w,
|
||||||
|
None => build_mini_player_window(&app, false)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let visible = win.is_visible().unwrap_or(false);
|
||||||
|
if visible {
|
||||||
|
win.hide().map_err(|e| e.to_string())?;
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
if let Some(main) = app.get_webview_window("main") {
|
||||||
|
let _ = main.unminimize();
|
||||||
|
let _ = main.show();
|
||||||
|
let _ = main.set_focus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Re-applying the saved position after show() — many Linux WMs
|
||||||
|
// (Mutter, KWin) re-centre hidden windows when they're shown
|
||||||
|
// again, ignoring any earlier set_position. Mark the move as
|
||||||
|
// programmatic so the Moved-event handler doesn't echo the
|
||||||
|
// intermediate centre coords back to disk.
|
||||||
|
let target = read_mini_pos(&app);
|
||||||
|
mark_mini_pos_programmatic();
|
||||||
|
win.show().map_err(|e| e.to_string())?;
|
||||||
|
let _ = win.set_focus();
|
||||||
|
if let Some(p) = target {
|
||||||
|
let _ = win.set_position(tauri::PhysicalPosition::new(p.x, p.y));
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
if let Some(main) = app.get_webview_window("main") {
|
||||||
|
let _ = main.minimize();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -3049,6 +3060,21 @@ pub fn run() {
|
|||||||
audio::start_device_watcher(&engine, app.handle().clone());
|
audio::start_device_watcher(&engine, app.handle().clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Pre-create mini player window (Windows) ──────────────────
|
||||||
|
// Creating the second WebView2 webview lazily from an invoke
|
||||||
|
// handler on Windows reliably stalls the Tauri event loop —
|
||||||
|
// the mini shows a blank white window, neither main nor mini
|
||||||
|
// can be closed, and the user has to kill the process via
|
||||||
|
// Task Manager. Building it at startup (hidden) avoids the
|
||||||
|
// runtime-creation code path entirely; later `open_mini_player`
|
||||||
|
// calls are pure show/hide.
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
if let Err(e) = build_mini_player_window(app.handle(), false) {
|
||||||
|
eprintln!("[psysonic] Failed to pre-create mini window: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cold start with `--player …`: defer emit so the webview can register listeners.
|
// Cold start with `--player …`: defer emit so the webview can register listeners.
|
||||||
crate::cli::spawn_deferred_cli_argv_handler(app.handle());
|
crate::cli::spawn_deferred_cli_argv_handler(app.handle());
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
|
|||||||
import { usePlayerStore } from '../store/playerStore';
|
import { usePlayerStore } from '../store/playerStore';
|
||||||
import { useKeybindingsStore, matchInAppBinding } from '../store/keybindingsStore';
|
import { useKeybindingsStore, matchInAppBinding } from '../store/keybindingsStore';
|
||||||
import { useDragDrop } from '../contexts/DragDropContext';
|
import { useDragDrop } from '../contexts/DragDropContext';
|
||||||
import { IS_MACOS } from '../utils/platform';
|
import { IS_LINUX } from '../utils/platform';
|
||||||
import MiniContextMenu from './MiniContextMenu';
|
import MiniContextMenu from './MiniContextMenu';
|
||||||
import type { MiniSyncPayload, MiniControlAction, MiniTrackInfo } from '../utils/miniPlayerBridge';
|
import type { MiniSyncPayload, MiniControlAction, MiniTrackInfo } from '../utils/miniPlayerBridge';
|
||||||
|
|
||||||
@@ -145,8 +145,16 @@ export default function MiniPlayer() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Announce to main window that we're mounted; it replies with a snapshot.
|
// Announce to main window that we're mounted; it replies with a snapshot.
|
||||||
|
// Also re-announce on window focus: on Windows the mini is pre-created at
|
||||||
|
// app startup so the mount-time emit can race past main's bridge before
|
||||||
|
// it has attached its listener. Re-emitting on focus means every actual
|
||||||
|
// open of the mini (user clicks the player-bar icon) triggers a fresh
|
||||||
|
// sync regardless of startup ordering.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
emit('mini:ready', {}).catch(() => {});
|
emit('mini:ready', {}).catch(() => {});
|
||||||
|
const onFocus = () => { emit('mini:ready', {}).catch(() => {}); };
|
||||||
|
window.addEventListener('focus', onFocus);
|
||||||
|
return () => window.removeEventListener('focus', onFocus);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Restore the expanded window size on initial mount when the queue was
|
// Restore the expanded window size on initial mount when the queue was
|
||||||
@@ -327,16 +335,17 @@ export default function MiniPlayer() {
|
|||||||
return (
|
return (
|
||||||
<div className="mini-player-shell">
|
<div className="mini-player-shell">
|
||||||
<div
|
<div
|
||||||
className={`mini-player__titlebar${IS_MACOS ? ' mini-player__titlebar--mac' : ''}`}
|
className={`mini-player__titlebar${!IS_LINUX ? ' mini-player__titlebar--mac' : ''}`}
|
||||||
{...(IS_MACOS ? {} : { 'data-tauri-drag-region': true })}
|
{...(!IS_LINUX ? {} : { 'data-tauri-drag-region': true })}
|
||||||
>
|
>
|
||||||
{!IS_MACOS ? (
|
{IS_LINUX ? (
|
||||||
<span className="mini-player__titlebar-title" data-tauri-drag-region>
|
<span className="mini-player__titlebar-title" data-tauri-drag-region>
|
||||||
{track?.title ?? 'Psysonic Mini'}
|
{track?.title ?? 'Psysonic Mini'}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
// macOS already shows the track title in the native titlebar; we
|
// macOS/Windows already render a native titlebar with the window
|
||||||
// just need a flexible spacer so the action buttons sit right.
|
// title + close button; we just need a flexible spacer so the
|
||||||
|
// action buttons sit right.
|
||||||
<span className="mini-player__titlebar-spacer" />
|
<span className="mini-player__titlebar-spacer" />
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
@@ -369,9 +378,9 @@ export default function MiniPlayer() {
|
|||||||
>
|
>
|
||||||
<Maximize2 size={13} />
|
<Maximize2 size={13} />
|
||||||
</button>
|
</button>
|
||||||
{/* macOS already provides Close via the red traffic light — skip
|
{/* macOS + Windows already provide Close via the native titlebar —
|
||||||
the duplicate so the in-app titlebar stays minimal. */}
|
skip the duplicate so the in-app titlebar stays minimal. */}
|
||||||
{!IS_MACOS && (
|
{IS_LINUX && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="mini-player__titlebar-btn mini-player__titlebar-btn--close"
|
className="mini-player__titlebar-btn mini-player__titlebar-btn--close"
|
||||||
|
|||||||
Reference in New Issue
Block a user