From f1628439beea854e81bc8801dc3737291b55f573 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Sun, 18 Oct 2026 07:33:58 +0000 Subject: [PATCH] playback: wrap adjacent artwork with repeat The now playing strip took its neighbours from previousItem() and upcomingItem(), which returned null at the queue edges online even when repeat would wrap. Swiping there resisted and only a hard fling advanced the track, leaving no artwork to slide in. Mirror next() and previousTrack() so the online path wraps with repeat All, and with repeat One for the next slot. --- .../subtracks/playback/PlaybackController.kt | 22 +++++++- .../playback/PlaybackControllerTest.kt | 55 +++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt b/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt index e306f8ef..ed426a78 100644 --- a/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt +++ b/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt @@ -934,7 +934,17 @@ class PlaybackController( } val snapshot = current ?: return null val at = position ?: return null - if (!isOffline) return queueRepository.itemAt(snapshot, at + 1)?.toQueueItem() + if (!isOffline) { + val next = + if (at + 1 < snapshot.size) { + at + 1 + } else if (repeatMode != RepeatMode.Off) { + 0L + } else { + null + } + return next?.let { queueRepository.itemAt(snapshot, it)?.toQueueItem() } + } downloads.awaitLoaded() val target = downloadedAtOrAfter(snapshot, at + 1) @@ -953,7 +963,15 @@ class PlaybackController( } val snapshot = current ?: return null val at = position ?: return null - if (!isOffline) return if (at <= 0) null else queueRepository.itemAt(snapshot, at - 1)?.toQueueItem() + if (!isOffline) { + val previous = + when { + at > 0 -> at - 1 + repeatMode == RepeatMode.All -> snapshot.size - 1 + else -> return null + } + return queueRepository.itemAt(snapshot, previous)?.toQueueItem() + } downloads.awaitLoaded() val target = downloadedAtOrBefore(snapshot, at - 1) diff --git a/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt b/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt index e7a7275e..2b1e6a33 100644 --- a/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt +++ b/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt @@ -218,6 +218,61 @@ class PlaybackControllerTest { assertNull(runBlocking { controller.previousItem() }) } + @Test + fun previousItemWrapsToTheEndWithRepeatAll() { + seedAlbum(3, sourceId = 1) + controller.playAlbum(1, "al1", 0) + await { + controller.state.value.item + ?.id == "s1" + } + controller.cycleRepeat() + assertEquals(RepeatMode.All, controller.state.value.repeat) + + assertEquals("s3", runBlocking { controller.previousItem()?.id }) + } + + @Test + fun upcomingItemIsNullAtTheEndOfTheQueue() { + seedAlbum(3, sourceId = 1) + controller.playAlbum(1, "al1", 2) + await { + controller.state.value.item + ?.id == "s3" + } + + assertNull(runBlocking { controller.upcomingItem() }) + } + + @Test + fun upcomingItemWrapsToTheStartWithRepeatAll() { + 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) + + assertEquals("s1", runBlocking { controller.upcomingItem()?.id }) + } + + @Test + fun upcomingItemWrapsToTheStartWithRepeatOne() { + seedAlbum(3, sourceId = 1) + controller.playAlbum(1, "al1", 2) + await { + controller.state.value.item + ?.id == "s3" + } + controller.cycleRepeat() + controller.cycleRepeat() + assertEquals(RepeatMode.One, controller.state.value.repeat) + + assertEquals("s1", runBlocking { controller.upcomingItem()?.id }) + } + @Test fun previousTrackMovesBackEvenOncePreviousWouldRestart() { seedAlbum(3, sourceId = 1)