diff --git a/crates/qbz-core/src/core.rs b/crates/qbz-core/src/core.rs index 96da5fea0..615de3e87 100644 --- a/crates/qbz-core/src/core.rs +++ b/crates/qbz-core/src/core.rs @@ -216,10 +216,13 @@ impl QbzCore { new_enabled } - /// Clear the queue - pub async fn clear_queue(&self) { + /// Clear the queue. `keep_current=true` preserves the now-playing track + /// (historical behavior); `false` wipes everything including the current + /// slot — use when nothing is actively playing and the user wants a full + /// reset. + pub async fn clear_queue(&self, keep_current: bool) { let queue = self.queue.write().await; - queue.clear(); + queue.clear(keep_current); self.emit(CoreEvent::QueueUpdated { state: queue.get_state(), }) diff --git a/crates/qbz-player/src/queue.rs b/crates/qbz-player/src/queue.rs index e696babfa..b8b002b80 100644 --- a/crates/qbz-player/src/queue.rs +++ b/crates/qbz-player/src/queue.rs @@ -190,12 +190,17 @@ impl QueueManager { Self::set_identity_shuffle_order_internal(&mut state); } - /// Clear the queue - pub fn clear(&self) { + /// Clear the queue. + /// + /// When `keep_current` is true (default / historical behavior), the track + /// at `current_index` is preserved as the sole remaining entry so the + /// "now playing" slot doesn't go dark mid-song. Callers that know nothing + /// is playing (or want to fully reset) can pass `false` to wipe everything, + /// including the current track. + pub fn clear(&self, keep_current: bool) { let mut state = self.state.lock().unwrap(); - // A track is currently playing, keep it as the only item left. - if let Some(_curr_idx) = state.current_index { + if keep_current && state.current_index.is_some() { state.tracks.truncate(1); state.current_index = Some(0); } else { @@ -912,7 +917,7 @@ mod tests { queue.add_track(create_test_track(124)); queue.add_track(create_test_track(125)); - queue.clear(); + queue.clear(true); let state = queue.get_state(); assert!(state.current_track.is_none()); @@ -929,7 +934,7 @@ mod tests { queue.add_track(create_test_track(125)); queue.play_index(0); - queue.clear(); + queue.clear(true); let state = queue.get_state(); assert!(state.current_track.is_some()); @@ -938,6 +943,24 @@ mod tests { assert_eq!(state.total_tracks, 1); } + #[test] + fn test_clear_wipes_current_track_when_not_kept() { + let queue = QueueManager::new(); + + queue.add_track(create_test_track(123)); + queue.add_track(create_test_track(124)); + queue.play_index(0); + + // keep_current: false — user pressed Clear Queue while nothing was + // actively playing, so the stale "now playing" slot should go too. + queue.clear(false); + + let state = queue.get_state(); + assert!(state.current_track.is_none()); + assert!(state.upcoming.is_empty()); + assert_eq!(state.total_tracks, 0); + } + #[test] fn test_clear_preserves_history() { let queue = QueueManager::new(); @@ -952,7 +975,7 @@ mod tests { assert_eq!(before.history.len(), 1); assert_eq!(before.history[0].id, 123); - queue.clear(); + queue.clear(true); let after = queue.get_state(); assert_eq!(after.history.len(), 1); diff --git a/crates/qbzd/src/api/queue.rs b/crates/qbzd/src/api/queue.rs index b0e304340..3caf13799 100644 --- a/crates/qbzd/src/api/queue.rs +++ b/crates/qbzd/src/api/queue.rs @@ -98,7 +98,8 @@ pub async fn move_track( } pub async fn clear(daemon: Arc) -> &'static str { - daemon.core.clear_queue().await; + // Daemon clear keeps the current track — matches legacy TUI behavior. + daemon.core.clear_queue(true).await; "ok" } diff --git a/src-tauri/src/commands_v2/queue.rs b/src-tauri/src/commands_v2/queue.rs index 3efab7d7a..b5f969bfa 100644 --- a/src-tauri/src/commands_v2/queue.rs +++ b/src-tauri/src/commands_v2/queue.rs @@ -134,9 +134,15 @@ pub async fn v2_set_shuffle( Ok(()) } -/// Clear the queue (V2 - uses QbzCore) +/// Clear the queue (V2 - uses QbzCore). +/// +/// `includeCurrent` (camelCase from frontend → `include_current` here): when +/// true, also wipes the now-playing slot and stops playback. Frontend sets +/// this when nothing is actively playing so the stale "now playing" track +/// doesn't linger after a clear. Defaults to false for historical behavior. #[tauri::command] pub async fn v2_clear_queue( + include_current: Option, bridge: State<'_, CoreBridgeState>, qconnect: State<'_, QconnectServiceState>, runtime: State<'_, RuntimeManagerState>, @@ -154,8 +160,14 @@ pub async fn v2_clear_queue( return Ok(()); } + let wipe_all = include_current.unwrap_or(false); let bridge = bridge.get().await; - bridge.clear_queue().await; + if wipe_all { + // Stop audio output first so the current track stops playing / being + // "loaded" before we wipe it from the queue state. + let _ = bridge.stop(); + } + bridge.clear_queue(!wipe_all).await; // Queue replaced — Mixtape context is no longer valid. runtime.manager().set_queue_source_collection(None).await; Ok(()) diff --git a/src-tauri/src/core_bridge.rs b/src-tauri/src/core_bridge.rs index 8b1196ece..4fd9c8bf3 100644 --- a/src-tauri/src/core_bridge.rs +++ b/src-tauri/src/core_bridge.rs @@ -144,9 +144,10 @@ impl CoreBridge { .await } - /// Clear the queue - pub async fn clear_queue(&self) { - self.core.clear_queue().await + /// Clear the queue. `keep_current` preserves the now-playing slot + /// (legacy default); passing `false` wipes everything for a full reset. + pub async fn clear_queue(&self, keep_current: bool) { + self.core.clear_queue(keep_current).await } /// Add a track to the end of the queue diff --git a/src-tauri/src/qconnect_service.rs b/src-tauri/src/qconnect_service.rs index dcafd061d..d250eb4e8 100644 --- a/src-tauri/src/qconnect_service.rs +++ b/src-tauri/src/qconnect_service.rs @@ -3102,7 +3102,8 @@ async fn materialize_remote_queue_to_corebridge( }; if queue_state.queue_items.is_empty() { - bridge.clear_queue().await; + // Preserve legacy behavior: keep current track on qconnect sync clears. + bridge.clear_queue(true).await; bridge.set_shuffle(false).await; let mut state = sync_state.lock().await; state.last_materialized_start_index = None;