diff --git a/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt b/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt index b6350f31..067062fc 100644 --- a/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt +++ b/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt @@ -52,6 +52,7 @@ data class PlaybackState( val hasPrevious: Boolean = false, val shuffle: Boolean = false, val repeat: RepeatMode = RepeatMode.Off, + val restored: Boolean = false, ) private sealed interface QueueUndo { @@ -127,6 +128,7 @@ class PlaybackController( private var repeatMode = RepeatMode.Off private var endedHandled = false private var lastSavedPositionMs = 0L + private var restored = false init { scope.launch { @@ -226,6 +228,7 @@ class PlaybackController( windowEnd = -1 lastEdit = null endedHandled = false + restored = false val previous = snapshot val previousEntry = previous?.entries?.firstOrNull()?.entry val same = previousEntry != null && previousEntry.kind == entry.kind && previousEntry.refId == entry.refId @@ -601,6 +604,7 @@ class PlaybackController( windowEnd = -1 lastEdit = null endedHandled = false + restored = false val modes = queueRepository.modes() shuffleEnabled = !disableShuffle && modes.shuffle repeatMode = modes.repeat.toRepeatMode() @@ -661,8 +665,8 @@ class PlaybackController( this.queueSourceId = queueSourceId val position = queueRepository.cursor().coerceIn(0, snapshot.size - 1) lastSavedPositionMs = queueRepository.cursorPositionMs().coerceAtLeast(0) + restored = true loadWindow(position, autoplay = false, startPositionMs = lastSavedPositionMs) - player?.prepare() } private suspend fun stop() = startLock.withLock { stopLocked() } @@ -677,6 +681,7 @@ class PlaybackController( windowStart = 0 windowEnd = -1 lastEdit = null + restored = false player?.run { stop() clear() @@ -701,10 +706,8 @@ class PlaybackController( updating = true player.setWindow(window.map { it.item.toQueueItem() }, startIndex, startPositionMs) updating = false - if (autoplay) { - player.prepare() - player.play() - } + player.prepare() + if (autoplay) player.play() refresh(position) } @@ -940,6 +943,7 @@ class PlaybackController( hasPrevious = true, shuffle = shuffleEnabled, repeat = repeatMode, + restored = restored, ) if (player.isPlaying) startPositionTicker() else stopPositionTicker() } diff --git a/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt b/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt index 6a8a493a..6ce91a2a 100644 --- a/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt +++ b/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt @@ -5,6 +5,7 @@ import androidx.activity.OnBackPressedCallback import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.compose.animation.core.animate import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.snap @@ -233,12 +234,9 @@ private fun MainNavigation() { val navBarInset = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() } val keyboardUp = WindowInsets.imeAnimationTarget.getBottom(density) > 0 val showMiniPlayer = playerVisible && !keyboardUp - var miniPlayerRevealed by remember { mutableStateOf(false) } - val firstReveal = !miniPlayerRevealed && showMiniPlayer - LaunchedEffect(showMiniPlayer) { if (showMiniPlayer) miniPlayerRevealed = true } val bottomInset by animateDpAsState( targetValue = if (showMiniPlayer) 0.dp else navBarInset, - animationSpec = if (firstReveal) snap() else tween(MINI_PLAYER_ANIM_MS), + animationSpec = if (playback.restored) snap() else tween(MINI_PLAYER_ANIM_MS), label = "libraryBottomInset", ) @@ -382,12 +380,17 @@ private fun MainNavigation() { AnimatedVisibility( visible = showMiniPlayer, enter = - if (firstReveal) { + if (playback.restored) { EnterTransition.None } else { expandVertically(tween(MINI_PLAYER_ANIM_MS), expandFrom = Alignment.Bottom) }, - exit = shrinkVertically(tween(MINI_PLAYER_ANIM_MS), shrinkTowards = Alignment.Bottom), + exit = + if (playback.restored) { + ExitTransition.None + } else { + shrinkVertically(tween(MINI_PLAYER_ANIM_MS), shrinkTowards = Alignment.Bottom) + }, ) { val positionMs by playbackController.positionMs.collectAsStateWithLifecycle() val miniArt = playbackController.coverArt(playback.item, thumbnail = true) diff --git a/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt b/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt index d596fbb7..bf886e3a 100644 --- a/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt +++ b/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt @@ -160,6 +160,20 @@ class PlaybackControllerTest { assertEquals(listOf("play"), handle.operations) } + @Test + fun restoringMarksTheStateUntilPlaybackStarts() { + seedAlbum(5, sourceId = 1) + runBlocking { queues.replace(listOf(queues.albumEntry(1, "al1"))) } + + controller.connect() + await { handle.items.isNotEmpty() } + + assertTrue(controller.state.value.restored) + + controller.playAlbum(1, "al1", 0) + await { !controller.state.value.restored } + } + @Test fun nextAdvancesTheCursor() { seedAlbum(3, sourceId = 1) @@ -1067,7 +1081,9 @@ class PlaybackControllerTest { assertEquals(33_000L, handle.positionMs) assertFalse(handle.playWhenReady) - assertTrue(handle.operations.drop(before).none { it.startsWith("prepare") || it.startsWith("play") }) + val reload = handle.operations.drop(before) + assertTrue("a reload must re-prepare, or the next play pays for it", reload.any { it == "prepare" }) + assertTrue(reload.none { it == "play" }) } @Test