From 5462b0f4ba666753457e711821f4c2c126bc9670 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Sat, 26 Sep 2026 20:55:25 +0000 Subject: [PATCH] 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). --- .../subtracks/playback/PlaybackController.kt | 2 +- .../com/subtracks/ui/playback/QueueScreen.kt | 13 +- .../playback/PlaybackControllerTest.kt | 113 ++++++++++++++++++ 3 files changed, 123 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt b/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt index 329be82d..336b718f 100644 --- a/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt +++ b/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt @@ -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) } } diff --git a/app/src/main/kotlin/com/subtracks/ui/playback/QueueScreen.kt b/app/src/main/kotlin/com/subtracks/ui/playback/QueueScreen.kt index d2eb6b53..8f03b908 100644 --- a/app/src/main/kotlin/com/subtracks/ui/playback/QueueScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/playback/QueueScreen.kt @@ -317,7 +317,11 @@ fun QueueScreen( var dragId by remember { mutableStateOf(null) } var dragFrom by remember { mutableStateOf(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( diff --git a/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt b/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt index 6e4d38b1..21fab716 100644 --- a/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt +++ b/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt @@ -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)