mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
fix the startup reveal and the first-play hitch for real
The mini player's enter spec was decided from `firstReveal`, which was false at the first composition (the player is not visible yet) and flipped back to `expandVertically` in the same frame the visibility changed, so the expand animation always ran. PlaybackState now carries `restored`, set by restore() and cleared on any user playback, and the mini player uses it to appear in place for the whole restored session. The first play after a restart could still pay for a fresh buffer because a stream-quality reload rebuilds the window, and loadWindow only prepared when autoplay was set. It now always prepares, so a reload (or the restore) leaves the current track buffered.
This commit is contained in:
@@ -52,6 +52,7 @@ data class PlaybackState(
|
|||||||
val hasPrevious: Boolean = false,
|
val hasPrevious: Boolean = false,
|
||||||
val shuffle: Boolean = false,
|
val shuffle: Boolean = false,
|
||||||
val repeat: RepeatMode = RepeatMode.Off,
|
val repeat: RepeatMode = RepeatMode.Off,
|
||||||
|
val restored: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
private sealed interface QueueUndo {
|
private sealed interface QueueUndo {
|
||||||
@@ -127,6 +128,7 @@ class PlaybackController(
|
|||||||
private var repeatMode = RepeatMode.Off
|
private var repeatMode = RepeatMode.Off
|
||||||
private var endedHandled = false
|
private var endedHandled = false
|
||||||
private var lastSavedPositionMs = 0L
|
private var lastSavedPositionMs = 0L
|
||||||
|
private var restored = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -226,6 +228,7 @@ class PlaybackController(
|
|||||||
windowEnd = -1
|
windowEnd = -1
|
||||||
lastEdit = null
|
lastEdit = null
|
||||||
endedHandled = false
|
endedHandled = false
|
||||||
|
restored = false
|
||||||
val previous = snapshot
|
val previous = snapshot
|
||||||
val previousEntry = previous?.entries?.firstOrNull()?.entry
|
val previousEntry = previous?.entries?.firstOrNull()?.entry
|
||||||
val same = previousEntry != null && previousEntry.kind == entry.kind && previousEntry.refId == entry.refId
|
val same = previousEntry != null && previousEntry.kind == entry.kind && previousEntry.refId == entry.refId
|
||||||
@@ -601,6 +604,7 @@ class PlaybackController(
|
|||||||
windowEnd = -1
|
windowEnd = -1
|
||||||
lastEdit = null
|
lastEdit = null
|
||||||
endedHandled = false
|
endedHandled = false
|
||||||
|
restored = false
|
||||||
val modes = queueRepository.modes()
|
val modes = queueRepository.modes()
|
||||||
shuffleEnabled = !disableShuffle && modes.shuffle
|
shuffleEnabled = !disableShuffle && modes.shuffle
|
||||||
repeatMode = modes.repeat.toRepeatMode()
|
repeatMode = modes.repeat.toRepeatMode()
|
||||||
@@ -661,8 +665,8 @@ class PlaybackController(
|
|||||||
this.queueSourceId = queueSourceId
|
this.queueSourceId = queueSourceId
|
||||||
val position = queueRepository.cursor().coerceIn(0, snapshot.size - 1)
|
val position = queueRepository.cursor().coerceIn(0, snapshot.size - 1)
|
||||||
lastSavedPositionMs = queueRepository.cursorPositionMs().coerceAtLeast(0)
|
lastSavedPositionMs = queueRepository.cursorPositionMs().coerceAtLeast(0)
|
||||||
|
restored = true
|
||||||
loadWindow(position, autoplay = false, startPositionMs = lastSavedPositionMs)
|
loadWindow(position, autoplay = false, startPositionMs = lastSavedPositionMs)
|
||||||
player?.prepare()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun stop() = startLock.withLock { stopLocked() }
|
private suspend fun stop() = startLock.withLock { stopLocked() }
|
||||||
@@ -677,6 +681,7 @@ class PlaybackController(
|
|||||||
windowStart = 0
|
windowStart = 0
|
||||||
windowEnd = -1
|
windowEnd = -1
|
||||||
lastEdit = null
|
lastEdit = null
|
||||||
|
restored = false
|
||||||
player?.run {
|
player?.run {
|
||||||
stop()
|
stop()
|
||||||
clear()
|
clear()
|
||||||
@@ -701,10 +706,8 @@ class PlaybackController(
|
|||||||
updating = true
|
updating = true
|
||||||
player.setWindow(window.map { it.item.toQueueItem() }, startIndex, startPositionMs)
|
player.setWindow(window.map { it.item.toQueueItem() }, startIndex, startPositionMs)
|
||||||
updating = false
|
updating = false
|
||||||
if (autoplay) {
|
|
||||||
player.prepare()
|
player.prepare()
|
||||||
player.play()
|
if (autoplay) player.play()
|
||||||
}
|
|
||||||
refresh(position)
|
refresh(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -940,6 +943,7 @@ class PlaybackController(
|
|||||||
hasPrevious = true,
|
hasPrevious = true,
|
||||||
shuffle = shuffleEnabled,
|
shuffle = shuffleEnabled,
|
||||||
repeat = repeatMode,
|
repeat = repeatMode,
|
||||||
|
restored = restored,
|
||||||
)
|
)
|
||||||
if (player.isPlaying) startPositionTicker() else stopPositionTicker()
|
if (player.isPlaying) startPositionTicker() else stopPositionTicker()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.activity.OnBackPressedCallback
|
|||||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.EnterTransition
|
import androidx.compose.animation.EnterTransition
|
||||||
|
import androidx.compose.animation.ExitTransition
|
||||||
import androidx.compose.animation.core.animate
|
import androidx.compose.animation.core.animate
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.animation.core.snap
|
import androidx.compose.animation.core.snap
|
||||||
@@ -233,12 +234,9 @@ private fun MainNavigation() {
|
|||||||
val navBarInset = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
|
val navBarInset = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
|
||||||
val keyboardUp = WindowInsets.imeAnimationTarget.getBottom(density) > 0
|
val keyboardUp = WindowInsets.imeAnimationTarget.getBottom(density) > 0
|
||||||
val showMiniPlayer = playerVisible && !keyboardUp
|
val showMiniPlayer = playerVisible && !keyboardUp
|
||||||
var miniPlayerRevealed by remember { mutableStateOf(false) }
|
|
||||||
val firstReveal = !miniPlayerRevealed && showMiniPlayer
|
|
||||||
LaunchedEffect(showMiniPlayer) { if (showMiniPlayer) miniPlayerRevealed = true }
|
|
||||||
val bottomInset by animateDpAsState(
|
val bottomInset by animateDpAsState(
|
||||||
targetValue = if (showMiniPlayer) 0.dp else navBarInset,
|
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",
|
label = "libraryBottomInset",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -382,12 +380,17 @@ private fun MainNavigation() {
|
|||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = showMiniPlayer,
|
visible = showMiniPlayer,
|
||||||
enter =
|
enter =
|
||||||
if (firstReveal) {
|
if (playback.restored) {
|
||||||
EnterTransition.None
|
EnterTransition.None
|
||||||
} else {
|
} else {
|
||||||
expandVertically(tween(MINI_PLAYER_ANIM_MS), expandFrom = Alignment.Bottom)
|
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 positionMs by playbackController.positionMs.collectAsStateWithLifecycle()
|
||||||
val miniArt = playbackController.coverArt(playback.item, thumbnail = true)
|
val miniArt = playbackController.coverArt(playback.item, thumbnail = true)
|
||||||
|
|||||||
@@ -160,6 +160,20 @@ class PlaybackControllerTest {
|
|||||||
assertEquals(listOf("play"), handle.operations)
|
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
|
@Test
|
||||||
fun nextAdvancesTheCursor() {
|
fun nextAdvancesTheCursor() {
|
||||||
seedAlbum(3, sourceId = 1)
|
seedAlbum(3, sourceId = 1)
|
||||||
@@ -1067,7 +1081,9 @@ class PlaybackControllerTest {
|
|||||||
|
|
||||||
assertEquals(33_000L, handle.positionMs)
|
assertEquals(33_000L, handle.positionMs)
|
||||||
assertFalse(handle.playWhenReady)
|
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
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user