playback: preserve detached fallback queue state

This commit is contained in:
austinried
2026-11-14 12:05:28 +00:00
parent 767c3db1ae
commit a9738c6311
2 changed files with 60 additions and 3 deletions
@@ -161,6 +161,7 @@ class PlaybackController(
private var windowFiltered = false private var windowFiltered = false
private var detachedWindow = false private var detachedWindow = false
private var detachedPosition: Long? = null private var detachedPosition: Long? = null
private var detachedFallbackPosition: Long? = null
init { init {
queueRepository.setResetListener { reset -> queueRepository.setResetListener { reset ->
@@ -1505,9 +1506,11 @@ class PlaybackController(
private fun markDetached( private fun markDetached(
window: List<QueueWindowItem>, window: List<QueueWindowItem>,
position: Long?, position: Long?,
fallbackPosition: Long?,
) { ) {
detachedWindow = true detachedWindow = true
detachedPosition = position detachedPosition = position
detachedFallbackPosition = fallbackPosition
windowFiltered = offline windowFiltered = offline
windowPositions = LongArray(window.size + 1) { if (it == 0) DETACHED_POSITION else window[it - 1].position } windowPositions = LongArray(window.size + 1) { if (it == 0) DETACHED_POSITION else window[it - 1].position }
windowStart = window.firstOrNull()?.position ?: 0L windowStart = window.firstOrNull()?.position ?: 0L
@@ -1517,6 +1520,7 @@ class PlaybackController(
private fun clearDetached() { private fun clearDetached() {
detachedWindow = false detachedWindow = false
detachedPosition = null detachedPosition = null
detachedFallbackPosition = null
windowFiltered = offline windowFiltered = offline
} }
@@ -1536,7 +1540,7 @@ class PlaybackController(
windowEnd = windowPositions.lastOrNull() ?: -1L windowEnd = windowPositions.lastOrNull() ?: -1L
} }
private fun currentQueuePosition(): Long? = currentPosition() ?: detachedPosition private fun currentQueuePosition(): Long? = currentPosition() ?: detachedPosition ?: detachedFallbackPosition
private suspend fun rebuildWindow( private suspend fun rebuildWindow(
window: List<QueueWindowItem>, window: List<QueueWindowItem>,
@@ -1586,7 +1590,7 @@ class PlaybackController(
if (!detachedWindow || currentPosition() != null) return false if (!detachedWindow || currentPosition() != null) return false
queueRepository.setCursor(position) queueRepository.setCursor(position)
rebuildDetachedSurvivors(snapshot, position) rebuildDetachedSurvivors(snapshot, position)
refresh(position) refresh(lastPosition)
return true return true
} }
@@ -1608,7 +1612,7 @@ class PlaybackController(
updating = false updating = false
} }
val next = window.firstOrNull()?.position val next = window.firstOrNull()?.position
markDetached(window, next) markDetached(window, next, nearest.takeIf { snapshot.size > 0L })
lastPosition = next lastPosition = next
} }
@@ -4021,6 +4021,59 @@ class PlaybackControllerTest {
assertEquals(0L, controller.state.value.position) assertEquals(0L, controller.state.value.position)
} }
@Test
fun offlineDetachedPlaybackWithoutAForwardSurvivorCanPlayPrevious() {
detachOfflineTrackWithoutAForwardSurvivor()
controller.previousTrack()
await { handle.currentItem?.id == "s1" && controller.state.value.position == 0L }
}
@Test
fun offlineDetachedPlaybackWithoutAForwardSurvivorCanRemoveAnEarlierTrack() {
detachOfflineTrackWithoutAForwardSurvivor()
runBlocking { controller.removeAt(0) }
val snapshot = runBlocking { queues.snapshot() }
assertEquals(1L, snapshot.size)
assertEquals("s3", runBlocking { queues.itemAt(snapshot, 0) }?.song?.id)
assertEquals(listOf("s2"), handle.items.map { it.id })
assertNull(controller.state.value.position)
}
@Test
fun offlineDetachedPlaybackWithoutAForwardSurvivorReloadsWhenOnline() {
detachOfflineTrackWithoutAForwardSurvivor()
setOnline()
await { handle.items.map { it.id } == listOf("s2", "s3") }
controller.next()
await { handle.currentItem?.id == "s3" && controller.state.value.position == 1L }
}
@Test
fun clearingTheFinalDetachedSurvivorPublishesNoQueuePosition() {
seedAlbum(1, sourceId = 1)
seedSong("x1", "al2")
controller.playAlbum(1, "al1", 0)
await { handle.currentItem?.id == "s1" && controller.state.value.isPlaying }
controller.addToQueue(1, QueueKind.Song, "x1")
await { runBlocking { queues.snapshot().upNextSize } == 1L }
val point = runBlocking { queues.captureResetPoint() }
runBlocking { db.libraryDao().deleteSongs(1, listOf("s1")) }
runBlocking { queues.resetChangedOrigins(setOf(QueueOrigin(QueueKind.Album, 1, "al1")), point) }
await { handle.items.map { it.id } == listOf("s1", "x1") && controller.state.value.position == 0L }
controller.clearUpNext()
await { handle.items.map { it.id } == listOf("s1") }
assertEquals(0L, runBlocking { queues.snapshot().size })
assertNull(controller.state.value.position)
}
@Test @Test
fun aResetRemapsAContextTrackThatMovedToUpNext() { fun aResetRemapsAContextTrackThatMovedToUpNext() {
seedPlaylist(5) seedPlaylist(5)