fix shuffled queue edits: flat remap, pinned shuffle start, stable highlight

The previous commit assumed a removed/moved track only shifts play
positions by one, but a shuffled context removal re-derives the
permutation, so the current track's play position is a remap of its flat
index. `removeAt` now maps the current's flat through the updated order
(shuffled) or shifts it (unshuffled), and re-anchors the up-next block to
the track the cursor lands on. This fixes removing any track while
shuffled picking a random track and the anchor removal not advancing to
the block.

Restore "shuffle starts at the top": pick the seed so the requested start
track maps to play position 0 (`pinnedSeed`), instead of leaving the
current at its derived position. Applies to shuffle-play, tapping a track
while shuffled, toggling shuffle on, and restore.

The queue view's playing highlight now requires both the combined position
and the song id, so a transient position mismatch no longer flashes the
row above as playing.
This commit is contained in:
austinried
2026-10-04 00:30:01 +00:00
parent e142148b5a
commit 6d25b31a9a
4 changed files with 114 additions and 27 deletions
@@ -2003,6 +2003,76 @@ class PlaybackControllerTest {
)
}
@Test
fun shufflePlayStartsAtTheTopOfTheQueue() {
seedAlbum(20, sourceId = 1)
controller.shuffleAlbum(1, "al1")
await { controller.state.value.item != null && controller.state.value.shuffle }
assertEquals(0L, controller.state.value.position)
val snapshot = runBlocking { queues.snapshot() }
assertEquals(
controller.state.value.item
?.id,
runBlocking { queues.itemAt(snapshot, 0)?.song?.id },
)
}
@Test
fun enablingShufflePinsThePlayingTrackToTheTop() {
seedAlbum(20, sourceId = 1)
controller.playAlbum(1, "al1", 7)
await {
controller.state.value.item
?.id == "s8"
}
controller.toggleShuffle()
await { controller.state.value.shuffle }
assertEquals(0L, controller.state.value.position)
assertEquals(
"s8",
controller.state.value.item
?.id,
)
}
@Test
fun removingAContextTrackWhileShuffledKeepsThePlayingTrackAndTheBlock() {
seedAlbum(6, sourceId = 1)
seedSong("x1", "al2")
controller.playAlbum(1, "al1", 2)
await {
controller.state.value.item
?.id == "s3"
}
controller.addToQueue(1, QueueKind.Song, "x1")
await { runBlocking { controller.upcomingItem()?.id } == "x1" }
controller.toggleShuffle()
await { controller.state.value.shuffle && runBlocking { controller.upcomingItem()?.id } == "x1" }
val currentId =
controller.state.value.item!!
.id
val before = runBlocking { queues.snapshot() }
val currentPosition = controller.state.value.position!!
val removePosition = (0 until before.size).first { !before.isUpNext(it) && it != currentPosition }
runBlocking { controller.removeAt(removePosition) }
await {
val snap = runBlocking { queues.snapshot() }
controller.state.value.item
?.id == currentId &&
runBlocking { queues.itemAt(snap, controller.state.value.position!!)?.song?.id } == currentId
}
await { runBlocking { controller.upcomingItem()?.id } == "x1" }
}
@Test
fun shufflingTheSameAlbumAgainPicksADifferentTrack() {
seedAlbum(5, sourceId = 1)
@@ -416,6 +416,7 @@ class AppScreenshotTest {
Fixtures.songItems.mapIndexed { index, item -> QueueRow(index.toLong(), index.toLong(), item) },
ready = true,
currentPosition = 1L,
currentSongId = Fixtures.songItems[1].song.id,
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
onBack = {},
onPlay = {},