playback: share edge wrapping with the art predictions

Lift the wrap arithmetic out of next(), previous() and previousTrack() into nextPosition()/previousPosition() so upcomingItem()/previousItem() cannot drift from the actions again. A one-track queue no longer predicts itself as its own neighbour, online or offline: the wrap would land on the current position, and the actions are a no-op there, so the strip offered a dead swipe.
This commit is contained in:
austinried
2026-10-18 08:15:44 +00:00
parent f1628439be
commit 4d08b8fb1e
2 changed files with 68 additions and 39 deletions
@@ -693,10 +693,27 @@ class PlaybackController(
} }
return return
} }
when { nextPosition(snapshot, current)?.let { jumpTo(it) }
current < snapshot.size - 1 -> jumpTo(current + 1)
repeatMode != RepeatMode.Off -> jumpTo(0)
} }
private fun nextPosition(
snapshot: QueueSnapshot,
current: Long,
): Long? =
when {
current < snapshot.size - 1 -> current + 1
repeatMode != RepeatMode.Off -> 0L
else -> null
}
private fun previousPosition(
snapshot: QueueSnapshot,
current: Long,
): Long? =
when {
current > 0 -> current - 1
repeatMode == RepeatMode.All -> snapshot.size - 1
else -> null
} }
private suspend fun consumeUpNext(position: Long) = private suspend fun consumeUpNext(position: Long) =
@@ -784,21 +801,14 @@ class PlaybackController(
} }
return return
} }
when { val target = previousPosition(snapshot, current)
current > 0 -> { if (target != null) {
jumpTo(current - 1) jumpTo(target)
} } else {
repeatMode == RepeatMode.All -> {
jumpTo(snapshot.size - 1)
}
else -> {
player.seekTo(0) player.seekTo(0)
refresh() refresh()
} }
} }
}
fun previousTrack() { fun previousTrack() {
val snapshot = snapshot ?: return val snapshot = snapshot ?: return
@@ -814,10 +824,7 @@ class PlaybackController(
} }
return return
} }
when { previousPosition(snapshot, current)?.let { jumpTo(it) }
current > 0 -> jumpTo(current - 1)
repeatMode == RepeatMode.All -> jumpTo(snapshot.size - 1)
}
} }
fun addToQueue( fun addToQueue(
@@ -935,21 +942,15 @@ class PlaybackController(
val snapshot = current ?: return null val snapshot = current ?: return null
val at = position ?: return null val at = position ?: return null
if (!isOffline) { if (!isOffline) {
val next = return nextPosition(snapshot, at)
if (at + 1 < snapshot.size) { ?.takeIf { it != at }
at + 1 ?.let { queueRepository.itemAt(snapshot, it)?.toQueueItem() }
} else if (repeatMode != RepeatMode.Off) {
0L
} else {
null
}
return next?.let { queueRepository.itemAt(snapshot, it)?.toQueueItem() }
} }
downloads.awaitLoaded() downloads.awaitLoaded()
val target = val target =
downloadedAtOrAfter(snapshot, at + 1) downloadedAtOrAfter(snapshot, at + 1)
?: if (repeatMode != RepeatMode.Off) downloadedAtOrAfter(snapshot, 0L) else null ?: if (repeatMode != RepeatMode.Off) downloadedAtOrAfter(snapshot, 0L) else null
return target?.let { queueRepository.itemAt(snapshot, it)?.toQueueItem() } return target?.takeIf { it != at }?.let { queueRepository.itemAt(snapshot, it)?.toQueueItem() }
} }
suspend fun previousItem(): QueueItem? { suspend fun previousItem(): QueueItem? {
@@ -964,19 +965,15 @@ class PlaybackController(
val snapshot = current ?: return null val snapshot = current ?: return null
val at = position ?: return null val at = position ?: return null
if (!isOffline) { if (!isOffline) {
val previous = return previousPosition(snapshot, at)
when { ?.takeIf { it != at }
at > 0 -> at - 1 ?.let { queueRepository.itemAt(snapshot, it)?.toQueueItem() }
repeatMode == RepeatMode.All -> snapshot.size - 1
else -> return null
}
return queueRepository.itemAt(snapshot, previous)?.toQueueItem()
} }
downloads.awaitLoaded() downloads.awaitLoaded()
val target = val target =
downloadedAtOrBefore(snapshot, at - 1) downloadedAtOrBefore(snapshot, at - 1)
?: if (repeatMode == RepeatMode.All) downloadedAtOrBefore(snapshot, snapshot.size - 1) else null ?: if (repeatMode == RepeatMode.All) downloadedAtOrBefore(snapshot, snapshot.size - 1) else null
return target?.let { queueRepository.itemAt(snapshot, it)?.toQueueItem() } return target?.takeIf { it != at }?.let { queueRepository.itemAt(snapshot, it)?.toQueueItem() }
} }
private fun play( private fun play(
@@ -2262,6 +2262,38 @@ class PlaybackControllerTest {
) )
} }
@Test
fun aSingleTrackQueueHasNoAdjacentArtwork() {
seedAlbum(1, sourceId = 1)
controller.playAlbum(1, "al1", 0)
await {
controller.state.value.item
?.id == "s1"
}
controller.cycleRepeat()
assertEquals(RepeatMode.All, controller.state.value.repeat)
assertNull(runBlocking { controller.upcomingItem() })
assertNull(runBlocking { controller.previousItem() })
}
@Test
fun aSingleDownloadedTrackHasNoAdjacentArtworkWhenOffline() {
seedAlbum(1, sourceId = 1)
markDownloaded(1, "s1")
setOffline()
controller.playAlbum(1, "al1", 0)
await {
controller.state.value.item
?.id == "s1"
}
controller.cycleRepeat()
assertEquals(RepeatMode.All, controller.state.value.repeat)
assertNull(runBlocking { controller.upcomingItem() })
assertNull(runBlocking { controller.previousItem() })
}
@Test @Test
fun repeatAllLoopsAfterEndingWithRepeatOff() { fun repeatAllLoopsAfterEndingWithRepeatOff() {
seedAlbum(2, sourceId = 1) seedAlbum(2, sourceId = 1)