feat(queue): invalidate stop-after marker on queue mutations

set_queue, set_queue_with_order, and clear unconditionally drop the
marker (queue replaced/emptied → marked track may not exist anymore).
remove_track and remove_upcoming_track conditionally clear the marker
when the removed track matches it. move_track and add_* are unchanged
— a moved track is still the same track ID, and adding doesn't touch
existing tracks.

Refs #356.

cc
This commit is contained in:
vicrodh committed 2026-04-28 13:16:40 -06:00
1 parent 23ebe9bf1f
commit 17dd52aa35
1 file changed
+78
+78
View File
@@ -125,6 +125,7 @@ impl QueueManager {
/// Set the entire queue (replaces existing)
pub fn set_queue(&self, new_tracks: Vec<QueueTrack>, start_index: Option<usize>) {
let mut state = self.state.lock().unwrap();
state.stop_after_track_id = None;
// Remap history by track id BEFORE replacing tracks so that legitimate
// plays survive queue version bumps / reorders. Entries whose track is
// no longer present are dropped. See bug #316.
@@ -166,6 +167,7 @@ impl QueueManager {
shuffle_order: Option<Vec<usize>>,
) {
let mut state = self.state.lock().unwrap();
state.stop_after_track_id = None;
// Remap history by track id BEFORE replacing tracks so that legitimate
// plays survive queue version bumps / reorders. Entries whose track is
// no longer present are dropped. See bug #316.
@@ -208,6 +210,7 @@ impl QueueManager {
/// including the current track.
pub fn clear(&self, keep_current: bool) {
let mut state = self.state.lock().unwrap();
state.stop_after_track_id = None;
if keep_current && state.current_index.is_some() {
state.tracks.truncate(1);
@@ -232,6 +235,11 @@ impl QueueManager {
let removed = state.tracks.remove(index);
// Invalidate marker if the removed track matches
if state.stop_after_track_id == Some(removed.id) {
state.stop_after_track_id = None;
}
// Adjust current index if needed
if let Some(curr_idx) = state.current_index {
if index < curr_idx {
@@ -290,6 +298,11 @@ impl QueueManager {
let removed = state.tracks.remove(actual_index);
// Invalidate marker if the removed track matches
if state.stop_after_track_id == Some(removed.id) {
state.stop_after_track_id = None;
}
if let Some(curr_idx) = state.current_index {
if actual_index < curr_idx {
state.current_index = Some(curr_idx - 1);
@@ -1615,4 +1628,69 @@ mod tests {
assert!(!fired);
}
// ============ Stop-After Marker — Invalidation on Queue Mutations ============
#[test]
fn test_set_queue_invalidates_marker() {
let queue = QueueManager::new();
queue.add_track(create_test_track(101));
queue.add_track(create_test_track(102));
queue.set_stop_after(102);
queue.set_queue(vec![create_test_track(201), create_test_track(202)], None);
assert_eq!(queue.get_stop_after(), None);
}
#[test]
fn test_clear_invalidates_marker() {
let queue = QueueManager::new();
queue.add_track(create_test_track(101));
queue.add_track(create_test_track(102));
queue.set_stop_after(102);
queue.clear(true);
assert_eq!(queue.get_stop_after(), None);
}
#[test]
fn test_remove_track_invalidates_marker_when_marked_track_removed() {
let queue = QueueManager::new();
queue.add_track(create_test_track(101));
queue.add_track(create_test_track(102));
queue.add_track(create_test_track(103));
queue.set_stop_after(102);
queue.remove_track(1); // removes track 102
assert_eq!(queue.get_stop_after(), None);
}
#[test]
fn test_remove_track_keeps_marker_when_other_track_removed() {
let queue = QueueManager::new();
queue.add_track(create_test_track(101));
queue.add_track(create_test_track(102));
queue.add_track(create_test_track(103));
queue.set_stop_after(102);
queue.remove_track(0); // removes track 101
assert_eq!(queue.get_stop_after(), Some(102));
}
#[test]
fn test_move_track_does_not_invalidate_marker() {
let queue = QueueManager::new();
queue.add_track(create_test_track(101));
queue.add_track(create_test_track(102));
queue.add_track(create_test_track(103));
queue.set_stop_after(102);
queue.move_track(1, 0); // 102 moves to position 0
assert_eq!(queue.get_stop_after(), Some(102));
}
}