keep the up-next block reachable when the playing context track is removed while shuffled

Review found that removing the playing context track while shuffled with a
non-empty up-next block stranded the block behind the cursor: the
repository moves `upNextAnchor` to the preceding track, but the cursor's
re-derived slot position can land after the block, so the queued tracks
were skipped. Re-anchor the block to the track the cursor lands on, same
as `playAt`/`toggleShuffle`.

- regression test loops the sequence over fresh random seeds and asserts
  the block stays reachable; fails without the fix ("stranded behind the
  cursor").
- `removingTheCurrentTrackWhileShuffled...` now asserts the canonical slot
  the current track occupies instead of recomputing the production position
  formula (which only checked the implementation against itself).
- `QueueRepository.removeAt` returns Unit; every caller discarded the
  snapshot and re-read it, so it no longer builds one.
This commit is contained in:
austinried
2026-10-04 00:30:01 +00:00
parent 93742e8584
commit e6386ebddd
3 changed files with 58 additions and 11 deletions
@@ -384,17 +384,17 @@ class QueueRepository(
suspend fun removeAt(
snapshot: QueueSnapshot,
position: Long,
): QueueSnapshot {
) {
if (snapshot.isUpNext(position)) {
removeUpNextAt(snapshot, snapshot.upNextIndex(position))
return snapshot()
return
}
val play = snapshot.contextPlay(position)
if (snapshot.shuffled) {
// The permutation is derived from (seed, size), so removing a track re-derives it at
// the smaller size; keep the same seed and store the new size so the queue stays
// shuffled. There is no stable "next" to preserve here.
val flat = snapshot.flatContext(play) ?: return snapshot
val flat = snapshot.flatContext(play) ?: return
write(compact(removeEntry(snapshot.entries, flat)).map { it.entry })
adjustAnchorOnRemove(flat)
setShuffleSize(contextSize())
@@ -403,7 +403,6 @@ class QueueRepository(
write(removed.map { it.entry })
adjustAnchorOnRemove(play)
}
return snapshot()
}
private suspend fun adjustAnchorOnRemove(removedFlat: Long) =
@@ -323,7 +323,7 @@ class PlaybackController(
val wasUpNext = snapshot.isUpNext(current)
val removed = if (position == current) null else queueRepository.itemAt(snapshot, position)?.toQueueItem()
queueRepository.removeAt(snapshot, position)
val updated = readSnapshot()
var updated = readSnapshot()
this.snapshot = updated
windowJob?.cancel()
if (updated.size == 0L) {
@@ -348,12 +348,23 @@ class PlaybackController(
} else {
(if (position < current) current - 1 else current)
}
val target =
var target =
(
currentId?.let {
if (wasUpNext) queueRepository.combinedIndexOf(updated, it) else queueRepository.combinedContextIndexOf(updated, it)
} ?: fallback
).coerceIn(0, updated.size - 1)
// Removing the playing context track while shuffled leaves the block anchored to some
// other track; re-anchor it to the track the cursor landed on so the queued tracks
// still play next rather than being stranded behind the cursor.
if (snapshot.shuffled && !wasUpNext && position == current && updated.upNextSize > 0L) {
val reanchored = queueRepository.reanchorFor(updated, target)
if (reanchored.changed) {
updated = readSnapshot()
this.snapshot = updated
target = reanchored.position
}
}
lastPosition = target
lastEdit =
if (removed == null) {