add repeat and shuffle modes and fix duplicate track ordering

This commit is contained in:
austinried
2026-10-04 00:29:55 +00:00
parent 5a1915d06b
commit a755de5356
13 changed files with 1227 additions and 46 deletions
@@ -285,7 +285,7 @@ class QueueRepositoryTest {
seedLibrary()
repository.snapshotAfter(listOf(repository.albumEntry(1, "al1")))
repository.move(from = 0, to = 2)
repository.move(repository.snapshot(), from = 0, to = 2)
assertEquals(listOf("s2", "s3", "s1"), resolveAll(repository.snapshot()))
}
@@ -296,7 +296,7 @@ class QueueRepositoryTest {
seedLibrary()
repository.snapshotAfter(listOf(repository.albumEntry(1, "al1"), repository.songEntry(1, "s4")))
repository.move(from = 0, to = 3)
repository.move(repository.snapshot(), from = 0, to = 3)
assertEquals(listOf("s2", "s3", "s4", "s1"), resolveAll(repository.snapshot()))
}
@@ -307,8 +307,8 @@ class QueueRepositoryTest {
seedLibrary()
repository.snapshotAfter(listOf(repository.albumEntry(1, "al1")))
assertFalse(repository.move(from = 5, to = 0))
assertTrue(repository.move(from = 0, to = 2))
assertFalse(repository.move(repository.snapshot(), from = 5, to = 0))
assertTrue(repository.move(repository.snapshot(), from = 0, to = 2))
assertEquals(listOf("s2", "s3", "s1"), resolveAll(repository.snapshot()))
}
@@ -18,6 +18,9 @@ class FakePlayerHandle : PlayerHandle {
@Volatile
var duration = 0L
@Volatile
var positionMs = 0L
private val listeners = CopyOnWriteArrayList<PlayerHandle.Listener>()
@Volatile
@@ -54,7 +57,7 @@ class FakePlayerHandle : PlayerHandle {
override val durationMs: Long get() = duration
override val currentPositionMs: Long get() = 0
override val currentPositionMs: Long get() = positionMs
override val currentItem: QueueItem? get() = items.getOrNull(index)
@@ -190,6 +193,10 @@ class FakePlayerHandle : PlayerHandle {
notifyEvents()
}
override fun setRepeatOne(enabled: Boolean) {
operations += "setRepeatOne($enabled)"
}
override fun addListener(listener: PlayerHandle.Listener) {
listeners += listener
}
@@ -169,7 +169,7 @@ class PlaybackControllerTest {
}
@Test
fun theLastTrackReportsNoNext() {
fun theLastTrackStillEnablesNext() {
seedAlbum(3, sourceId = 1)
controller.playAlbum(1, "al1", 2)
@@ -178,7 +178,7 @@ class PlaybackControllerTest {
?.id == "s3"
}
assertFalse(controller.state.value.hasNext)
assertTrue(controller.state.value.hasNext)
assertTrue(controller.state.value.hasPrevious)
}
@@ -308,7 +308,7 @@ class PlaybackControllerTest {
assertEquals(QueueContext(QueueKind.Album, "al1"), controller.state.value.context)
assertTrue(controller.state.value.isPlaying)
assertTrue(controller.state.value.hasNext)
assertFalse(controller.state.value.hasPrevious)
assertTrue(controller.state.value.hasPrevious)
}
@Test
@@ -371,7 +371,7 @@ class PlaybackControllerTest {
assertEquals(0L, controller.state.value.position)
assertTrue(handle.operations.any { it.startsWith("setWindow") })
assertFalse(controller.state.value.hasPrevious)
assertTrue(controller.state.value.hasPrevious)
}
@Test
@@ -595,6 +595,137 @@ class PlaybackControllerTest {
assertEquals(listOf("s1", "s2", "s3"), handle.items.map { it.id })
}
@Test
fun shuffleKeepsTheCurrentTrackAndRestoresTheOriginalOrder() {
seedAlbum(5, sourceId = 1)
controller.playAlbum(1, "al1", 2)
await {
controller.state.value.item
?.id == "s3"
}
controller.toggleShuffle()
await { controller.state.value.shuffle }
assertEquals(
"s3",
controller.state.value.item
?.id,
)
assertEquals(0L, controller.state.value.position)
assertEquals("s3", handle.items.first().id)
controller.toggleShuffle()
await { !controller.state.value.shuffle }
assertEquals(listOf("s1", "s2", "s3", "s4", "s5"), handle.items.map { it.id })
assertEquals(
"s3",
controller.state.value.item
?.id,
)
}
@Test
fun nextWrapsToTheStartWhenRepeating() {
seedAlbum(3, sourceId = 1)
controller.playAlbum(1, "al1", 2)
await {
controller.state.value.item
?.id == "s3"
}
controller.cycleRepeat()
assertEquals(RepeatMode.All, controller.state.value.repeat)
controller.next()
await {
controller.state.value.item
?.id == "s1"
}
}
@Test
fun nextOnTheLastTrackDoesNothingWithoutRepeat() {
seedAlbum(3, sourceId = 1)
controller.playAlbum(1, "al1", 2)
await {
controller.state.value.item
?.id == "s3"
}
controller.next()
Thread.sleep(100)
assertEquals(
"s3",
controller.state.value.item
?.id,
)
}
@Test
fun previousRestartsTheTrackWhenPastTheThreshold() {
seedAlbum(3, sourceId = 1)
controller.playAlbum(1, "al1", 1)
await {
controller.state.value.item
?.id == "s2"
}
handle.positionMs = 5_000L
controller.previous()
assertTrue(handle.operations.contains("seekTo(0)"))
assertEquals(
"s2",
controller.state.value.item
?.id,
)
}
@Test
fun previousFromTheStartWrapsToTheLastTrack() {
seedAlbum(3, sourceId = 1)
controller.playAlbum(1, "al1", 0)
await {
controller.state.value.item
?.id == "s1"
}
controller.previous()
await {
controller.state.value.item
?.id == "s3"
}
}
@Test
fun repeatAllRestartsTheQueueWhenItEnds() {
seedAlbum(3, sourceId = 1)
controller.playAlbum(1, "al1", 2)
await {
controller.state.value.item
?.id == "s3"
}
controller.cycleRepeat()
assertEquals(RepeatMode.All, controller.state.value.repeat)
handle.finish()
handle.emitEvents()
await {
controller.state.value.item
?.id == "s1"
}
}
private fun seedAlbum(
count: Int,
sourceId: Long,