fix up-next drag and previous behaviour

- set lastPosition before seeking in jumpTo. With Dispatchers.Main
  .immediate the media transition runs synchronously inside the seek, so
  onPositionChanged saw the old position as an auto-advance and consumed
  the track you were leaving; previous from a queued track removed it and
  played the next one.
- give the up-next and context sections their own reorderable states. The
  library only targets items whose key is in that state's reorderableKeys,
  so a drag can no longer target a row in the other section: no more
  fighting the section boundary when dragging to the top and no no-op drop
  below the block.
- disable context reordering while shuffled via ReorderableItem(enabled).
This commit is contained in:
austinried
2026-10-04 00:30:00 +00:00
parent e7a1927ccd
commit 5462b0f4ba
3 changed files with 123 additions and 5 deletions
@@ -870,12 +870,12 @@ class PlaybackController(
endedHandled = false
return@withLock
}
lastPosition = dest
shiftWindowLocked(dest)
player.ensurePrepared()
player.seekToIndex((dest - windowStart).toInt())
if (wasEnded) player.play()
endedHandled = false
lastPosition = dest
refresh(dest)
}
}
@@ -317,7 +317,11 @@ fun QueueScreen(
var dragId by remember { mutableStateOf<Long?>(null) }
var dragFrom by remember { mutableStateOf<Long?>(null) }
val reorderState =
val upNextReorder =
rememberReorderableLazyListState(listState) { from, to ->
onReorder(from.index, to.index)
}
val contextReorder =
rememberReorderableLazyListState(listState) { from, to ->
onReorder(from.index, to.index)
}
@@ -378,10 +382,11 @@ fun QueueScreen(
if (index == 0 || rows[index - 1].upNext != row.upNext) {
QueueSectionHeader(if (row.upNext) "Up next" else contextTitle ?: "Next up")
}
val draggable = row.upNext || !shuffle
val enabled = row.upNext || !shuffle
ReorderableItem(
state = reorderState,
state = if (row.upNext) upNextReorder else contextReorder,
key = row.id,
enabled = enabled,
animateItemModifier =
Modifier.animateItem(
fadeInSpec = tween(150),
@@ -395,7 +400,7 @@ fun QueueScreen(
floating = isDragging,
coverArt = coverArt,
dragHandle =
if (!draggable) {
if (!enabled) {
null
} else {
Modifier.draggableHandle(
@@ -612,6 +612,119 @@ class PlaybackControllerTest {
assertTrue(controller.state.value.layout > before)
}
@Test
fun previousFromADeepQueuedTrackGoesBackWithoutConsuming() {
seedAlbum(3, sourceId = 1)
seedSong("q1", "al2")
seedSong("q2", "al2")
seedSong("q3", "al2")
controller.playAlbum(1, "al1", 0)
await {
controller.state.value.item
?.id == "s1"
}
controller.addToQueue(1, QueueKind.Song, "q1")
controller.addToQueue(1, QueueKind.Song, "q2")
controller.addToQueue(1, QueueKind.Song, "q3")
await { runBlocking { controller.upcomingItem()?.id } == "q1" }
controller.playAt(2)
await {
controller.state.value.item
?.id == "q2"
}
controller.previous()
await {
controller.state.value.item
?.id == "q1"
}
await { runBlocking { controller.upcomingItem()?.id } == "q2" }
}
@Test
fun previousFromAQueuedTrackDoesNotConsumeItWithAnEagerDispatcher() {
seedAlbum(3, sourceId = 1)
seedSong("q1", "al2")
seedSong("q2", "al2")
seedSong("q3", "al2")
val eagerHandle = FakePlayerHandle()
val inline =
object : kotlinx.coroutines.CoroutineDispatcher() {
override fun dispatch(
context: kotlin.coroutines.CoroutineContext,
block: Runnable,
) = block.run()
}
val eager = PlaybackController(sources, queues, FakePlayerConnection(eagerHandle), dispatcher = inline)
eager.playAlbum(1, "al1", 0)
await {
eager.state.value.item
?.id == "s1"
}
eager.addToQueue(1, QueueKind.Song, "q1")
eager.addToQueue(1, QueueKind.Song, "q2")
eager.addToQueue(1, QueueKind.Song, "q3")
await { runBlocking { eager.upcomingItem()?.id } == "q1" }
eager.playAt(2)
await {
eager.state.value.item
?.id == "q2"
}
eager.previous()
await {
eager.state.value.item
?.id == "q1"
}
Thread.sleep(300)
assertEquals(
"q1",
eager.state.value.item
?.id,
)
assertEquals("q2", runBlocking { eager.upcomingItem()?.id })
eager.close()
}
@Test
fun nextFollowsTheReorderedQueue() {
seedAlbum(3, sourceId = 1)
seedSong("q1", "al2")
seedSong("q2", "al2")
controller.playAlbum(1, "al1", 0)
await {
controller.state.value.item
?.id == "s1"
}
controller.addToQueue(1, QueueKind.Song, "q1")
controller.addToQueue(1, QueueKind.Song, "q2")
await { runBlocking { controller.upcomingItem()?.id } == "q1" }
runBlocking { controller.move(2, 1) }
await { runBlocking { controller.upcomingItem()?.id } == "q2" }
controller.next()
await {
controller.state.value.item
?.id == "q2"
}
controller.next()
await {
controller.state.value.item
?.id == "q1"
}
controller.next()
await {
controller.state.value.item
?.id == "s2"
}
}
@Test
fun theDurationUsesTheNextTracksMetadataWhileItLoads() {
seedAlbum(3, sourceId = 1)