fix(queue): wire the Remove all after and Add to playlist row actions
The per-track context menu in the Queue sidebar had two placeholder items with no click handler: "Remove all after" and "Add to playlist". Wire both end to end. - QueueState gains remove-all-after(int) and add-to-playlist(int), resolved from the page-local row index like remove-upcoming (search-filter aware). - Core/player gain remove_upcoming_after(upcoming_index): truncates the upcoming list in PLAY order (shuffle-aware, reuses remove_upcoming_track), clears the stop-after marker if it lands in the removed range, and emits a single QueueUpdated. The pre-existing remove_after (absolute-index, not shuffle-aware) was dead code and stays only for its unit coverage. - Add to playlist reuses the save-as-playlist picker handoff seeded with the single row's track id. cc
This commit is contained in:
6 files changed
+220
-1
No files matched your search
@@ -379,6 +379,54 @@ impl QueueManager {
|
||||
Some(removed)
|
||||
}
|
||||
|
||||
/// Number of upcoming tracks (those after the current one) in the current
|
||||
/// play order. Mirrors `get_state_full`'s upcoming computation: shuffle-aware
|
||||
/// when shuffle is on, otherwise the tail of `tracks` after `current_index`.
|
||||
fn upcoming_len(state: &InternalState) -> usize {
|
||||
match state.current_index {
|
||||
Some(curr) => {
|
||||
if state.shuffle {
|
||||
state
|
||||
.shuffle_order
|
||||
.len()
|
||||
.saturating_sub(state.shuffle_position + 1)
|
||||
} else {
|
||||
state.tracks.len().saturating_sub(curr + 1)
|
||||
}
|
||||
}
|
||||
None => state.tracks.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove every upcoming track positioned AFTER `upcoming_index` in the
|
||||
/// current play order; the track at `upcoming_index` is kept. Works in
|
||||
/// UPCOMING space (not absolute `tracks` indices), so it stays correct under
|
||||
/// shuffle by reusing `remove_upcoming_track`, which resolves upcoming
|
||||
/// positions through `shuffle_order`. Peels positions off the tail inward so
|
||||
/// the surviving positions never shift under it. Returns the count removed.
|
||||
///
|
||||
/// This is the wired "Remove all after" queue action. (`remove_after`, below,
|
||||
/// truncates by absolute `tracks` index and is NOT play-order-aware under
|
||||
/// shuffle — it is kept only for its existing unit coverage.)
|
||||
pub fn remove_upcoming_after(&self, upcoming_index: usize) -> usize {
|
||||
let mut upcoming_len = {
|
||||
let state = self.state.lock().unwrap();
|
||||
Self::upcoming_len(&state)
|
||||
};
|
||||
if upcoming_index + 1 >= upcoming_len {
|
||||
return 0;
|
||||
}
|
||||
let mut removed = 0usize;
|
||||
while upcoming_len > upcoming_index + 1 {
|
||||
if self.remove_upcoming_track(upcoming_len - 1).is_none() {
|
||||
break;
|
||||
}
|
||||
removed += 1;
|
||||
upcoming_len -= 1;
|
||||
}
|
||||
removed
|
||||
}
|
||||
|
||||
/// Remove all tracks at indices greater than `index`. The track at
|
||||
/// `index` is preserved. Returns the number of tracks removed.
|
||||
/// If the marker referenced a track in the removed range, the marker
|
||||
@@ -2085,6 +2133,86 @@ mod tests {
|
||||
assert_eq!(queue.get_stop_after(), Some(102));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_upcoming_after_linear() {
|
||||
let queue = QueueManager::new();
|
||||
// 101 playing, upcoming = [102, 103, 104, 105].
|
||||
queue.set_queue(
|
||||
(101..=105).map(create_test_track).collect(),
|
||||
Some(0),
|
||||
);
|
||||
|
||||
// Keep upcoming positions 0..=1 (102, 103); drop 2, 3 (104, 105).
|
||||
let removed = queue.remove_upcoming_after(1);
|
||||
|
||||
assert_eq!(removed, 2);
|
||||
let state = queue.get_state_full();
|
||||
assert_eq!(state.current_track.map(|t| t.id), Some(101));
|
||||
assert_eq!(
|
||||
state.upcoming.iter().map(|t| t.id).collect::<Vec<_>>(),
|
||||
vec![102, 103]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_upcoming_after_on_last_upcoming_is_noop() {
|
||||
let queue = QueueManager::new();
|
||||
queue.set_queue((101..=103).map(create_test_track).collect(), Some(0));
|
||||
// Upcoming = [102, 103]; position 1 is the last, nothing after it.
|
||||
let removed = queue.remove_upcoming_after(1);
|
||||
assert_eq!(removed, 0);
|
||||
assert_eq!(queue.get_state().total_tracks, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_upcoming_after_no_current_track() {
|
||||
let queue = QueueManager::new();
|
||||
// No current index -> every track is upcoming.
|
||||
queue.set_queue((101..=104).map(create_test_track).collect(), None);
|
||||
let removed = queue.remove_upcoming_after(0);
|
||||
assert_eq!(removed, 3);
|
||||
let state = queue.get_state_full();
|
||||
assert_eq!(
|
||||
state.upcoming.iter().map(|t| t.id).collect::<Vec<_>>(),
|
||||
vec![101]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_upcoming_after_respects_shuffle_play_order() {
|
||||
let queue = QueueManager::new();
|
||||
// tracks: 101..105 at indices 0..4. Shuffle play order = [2,0,4,1,3]
|
||||
// -> 103(current), then upcoming 101, 105, 102, 104.
|
||||
queue.set_queue_with_order(
|
||||
(101..=105).map(create_test_track).collect(),
|
||||
Some(2),
|
||||
true,
|
||||
Some(vec![2, 0, 4, 1, 3]),
|
||||
);
|
||||
|
||||
// Keep upcoming positions 0..=1 (101, 105); drop 2, 3 (102, 104).
|
||||
let removed = queue.remove_upcoming_after(1);
|
||||
|
||||
assert_eq!(removed, 2);
|
||||
let state = queue.get_state_full();
|
||||
assert_eq!(state.current_track.map(|t| t.id), Some(103));
|
||||
assert_eq!(
|
||||
state.upcoming.iter().map(|t| t.id).collect::<Vec<_>>(),
|
||||
vec![101, 105]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_upcoming_after_invalidates_marker_in_removed_range() {
|
||||
let queue = QueueManager::new();
|
||||
queue.set_queue((101..=105).map(create_test_track).collect(), Some(0));
|
||||
queue.set_stop_after(104); // upcoming position 2 -> removed by after(1)
|
||||
|
||||
queue.remove_upcoming_after(1);
|
||||
|
||||
assert_eq!(queue.get_stop_after(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_state_includes_stop_after() {
|
||||
let queue = QueueManager::new();
|
||||
|
||||
Reference in new issue
Block a user