diff --git a/crates/qbz-slint/src/main.rs b/crates/qbz-slint/src/main.rs index 111219bb9..8de151af8 100644 --- a/crates/qbz-slint/src/main.rs +++ b/crates/qbz-slint/src/main.rs @@ -3012,11 +3012,13 @@ fn main() -> Result<(), Box> { // renderer/local playback because the gate is `is_peer_renderer_active`. { let runtime = app_runtime.clone(); + let weak = window.as_weak(); let handle = tokio_rt.handle().clone(); window .global::() .on_toggle_play(move || { let runtime = runtime.clone(); + let weak = weak.clone(); let handle = handle.clone(); handle.clone().spawn(async move { if let Some(svc) = qconnect_service::service() { @@ -3029,7 +3031,7 @@ fn main() -> Result<(), Box> { } } } - playback::toggle_play_pause(runtime, handle); + playback::toggle_play_pause(runtime, weak, handle); }); }); } diff --git a/crates/qbz-slint/src/playback.rs b/crates/qbz-slint/src/playback.rs index 4c7c46c62..0c5eb3845 100644 --- a/crates/qbz-slint/src/playback.rs +++ b/crates/qbz-slint/src/playback.rs @@ -1868,16 +1868,50 @@ pub fn enqueue_local_tracks( } /// Toggle play / pause on the live player. -pub fn toggle_play_pause(runtime: Runtime, handle: tokio::runtime::Handle) { +/// +/// Resume is only valid when the audio engine actually holds a loaded stream. +/// When the player has NO loaded audio but the queue has a current track — +/// e.g. a freshly materialized QConnect renderer queue whose cursor sits on a +/// track that was never loaded, or a cold cursor after the queue ended — a +/// bare `resume()` fails with "cannot resume - no audio data available" and the +/// user sees a dead Play button. In that case LOAD and play the current queue +/// track instead, so Play works from a cold cursor. A normal pause leaves the +/// stream loaded (`has_loaded_audio` stays true), so the pause/resume path is +/// unchanged. +pub fn toggle_play_pause( + runtime: Runtime, + weak: slint::Weak, + handle: tokio::runtime::Handle, +) { handle.spawn(async move { - let playing = runtime.core().get_playback_state().is_playing; - let result = if playing { - runtime.core().pause() - } else { - runtime.core().resume() - }; - if let Err(e) = result { - log::error!("[qbz-slint] playback: toggle play/pause failed: {e}"); + if runtime.core().get_playback_state().is_playing { + if let Err(e) = runtime.core().pause() { + log::error!("[qbz-slint] playback: pause failed: {e}"); + } + return; + } + // Not playing: resume an existing stream, or cold-start the current + // queue track when nothing is loaded. + if runtime.core().player().has_loaded_audio() { + if let Err(e) = runtime.core().resume() { + log::error!("[qbz-slint] playback: resume failed: {e}"); + } + return; + } + match runtime.core().current_track().await { + Some(track) => { + log::info!( + "[qbz-slint] playback: play with no loaded audio -> cold-starting current track {}", + track.id + ); + after_track_change(&runtime, &weak, track.id).await; + refresh_sidebar(true); + } + None => { + log::info!( + "[qbz-slint] playback: toggle play ignored (no loaded audio, empty queue)" + ); + } } }); }