keep the block in place when the anchor is removed while a queued track plays

`adjustAnchorOnRemove` moved the anchor to the canonical predecessor of the
removed track, which in a shuffled queue is an unrelated play position, so
removing the context anchor while playing a queued track teleported the
up-next block somewhere random (and out of view).

When the removed context track is the anchor, move the anchor to the track
before it in play order (or -1), so the block stays where it sits. This also
covers removing the playing context anchor, so the controller's separate
re-anchor is no longer needed and is removed.
This commit is contained in:
austinried
2026-10-04 00:30:01 +00:00
parent 72330387c8
commit b21c297964
3 changed files with 35 additions and 13 deletions
@@ -465,9 +465,17 @@ class QueueRepository(
// tracks that remain keep their positions.
val order = snapshot.shuffle ?: return
val flat = snapshot.flatContext(play) ?: return
val predecessor = if (play <= 0L) -1L else snapshot.flatContext(play - 1L) ?: -1L
shuffleRemoved = order.withRemoved(order.domainFor(flat))
write(compact(removeEntry(snapshot.entries, flat)).map { it.entry })
adjustAnchorOnRemove(flat)
if (cursorRow().upNextAnchor == flat) {
// The removed track carried the block; move the anchor to the track before it in
// play order so the block stays in place instead of jumping to the canonical
// predecessor, which in a shuffled queue is an unrelated position.
setUpNextAnchor(if (predecessor > flat) predecessor - 1 else predecessor)
} else {
adjustAnchorOnRemove(flat)
}
} else {
val removed = compact(removeEntry(snapshot.entries, play))
write(removed.map { it.entry })
@@ -344,18 +344,6 @@ class PlaybackController(
lastEdit = ReloadUndo(entries, cursor, snapshot.shuffleSeed, upNext, snapshot.upNextAnchor)
return@withLock
}
// The block floats behind the currently playing context track, so removing that track
// removes its anchor. Re-anchor the block to the track that now precedes the removed
// slot in play order, so it takes the slot and plays next, rather than the anchor
// landing wherever `adjustAnchorOnRemove` left it.
if (snapshot.shuffled && position == current && !wasUpNext && updated.upNextSize > 0L) {
val play = snapshot.contextPlay(current)
val predecessor = play - 1L
val anchor = if (predecessor < 0L) -1L else updated.flatContext(predecessor) ?: -1L
queueRepository.setUpNextAnchor(anchor)
updated = readSnapshot()
this.snapshot = updated
}
// Derive the current track's new position from the edit. Resolving its song id after
// the edit can match an identical song elsewhere (a duplicate in the context or the
// block), so use positions throughout. Removing the playing track resumes at the same
@@ -1670,6 +1670,32 @@ class PlaybackControllerTest {
)
}
@Test
fun removingTheAnchorWhilePlayingABlockTrackKeepsTheBlockInPlace() {
seedShuffledQueue(seed = 5L)
seedSong("x1", "al2")
controller.addToQueue(1, QueueKind.Song, "x1")
await { runBlocking { controller.upcomingItem()?.id } == "x1" }
controller.playAt(1)
await {
controller.state.value.item
?.id == "x1"
}
val before = runBlocking { queues.snapshot() }
runBlocking { controller.removeAt(before.anchorPlay) }
val after = runBlocking { queues.snapshot() }
assertEquals(-1L, after.anchorPlay)
assertEquals("x1", runBlocking { queues.itemAt(after, 0)?.song?.id })
assertEquals(
"x1",
controller.state.value.item
?.id,
)
}
@Test
fun removingThePlayingTrackWhileShuffledPlaysTheNextShuffledTrack() {
seedShuffledQueue(seed = 12345L)