mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
gate the library on playback restore so the bottom bar lands in place
The tab bar was laid out before playback was known, so the restored mini player arrived later and re-laid the whole bottom area: a couple of frames of bump, and the mini player "loading in" after the library. SubtracksRoot now waits for PlaybackController to finish connecting and restoring (a `ready` flag, set after restore or on disconnect) before it composes the library, so the first library frame already includes the mini player and nothing moves. With playback resolved up front, the `restored` flag and its special enter/inset specs are pointless, so they are reverted; playback the user starts animates as before.
This commit is contained in:
@@ -52,7 +52,6 @@ data class PlaybackState(
|
||||
val hasPrevious: Boolean = false,
|
||||
val shuffle: Boolean = false,
|
||||
val repeat: RepeatMode = RepeatMode.Off,
|
||||
val restored: Boolean = false,
|
||||
)
|
||||
|
||||
private sealed interface QueueUndo {
|
||||
@@ -128,7 +127,6 @@ class PlaybackController(
|
||||
private var repeatMode = RepeatMode.Off
|
||||
private var endedHandled = false
|
||||
private var lastSavedPositionMs = 0L
|
||||
private var restored = false
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
@@ -152,6 +150,9 @@ class PlaybackController(
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
private val _ready = MutableStateFlow(false)
|
||||
val ready: StateFlow<Boolean> = _ready
|
||||
|
||||
fun connect() {
|
||||
if (player != null || connecting) return
|
||||
connecting = true
|
||||
@@ -170,12 +171,14 @@ class PlaybackController(
|
||||
shuffle != null -> startShuffled(shuffle)
|
||||
else -> restore()
|
||||
}
|
||||
_ready.value = true
|
||||
}
|
||||
},
|
||||
onDisconnected = {
|
||||
player = null
|
||||
connecting = false
|
||||
scope.launch { stop() }
|
||||
_ready.value = true
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -228,7 +231,6 @@ 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
|
||||
@@ -604,7 +606,6 @@ class PlaybackController(
|
||||
windowEnd = -1
|
||||
lastEdit = null
|
||||
endedHandled = false
|
||||
restored = false
|
||||
val modes = queueRepository.modes()
|
||||
shuffleEnabled = !disableShuffle && modes.shuffle
|
||||
repeatMode = modes.repeat.toRepeatMode()
|
||||
@@ -665,7 +666,6 @@ 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)
|
||||
}
|
||||
|
||||
@@ -681,7 +681,6 @@ class PlaybackController(
|
||||
windowStart = 0
|
||||
windowEnd = -1
|
||||
lastEdit = null
|
||||
restored = false
|
||||
player?.run {
|
||||
stop()
|
||||
clear()
|
||||
@@ -943,7 +942,6 @@ class PlaybackController(
|
||||
hasPrevious = true,
|
||||
shuffle = shuffleEnabled,
|
||||
repeat = repeatMode,
|
||||
restored = restored,
|
||||
)
|
||||
if (player.isPlaying) startPositionTicker() else stopPositionTicker()
|
||||
}
|
||||
|
||||
@@ -4,11 +4,8 @@ import android.net.Uri
|
||||
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
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
@@ -143,9 +140,20 @@ internal fun focusPops(
|
||||
@Composable
|
||||
fun SubtracksRoot(root: RootViewModel = koinViewModel()) {
|
||||
when (val hasSource = root.hasSource.collectAsStateWithLifecycle().value) {
|
||||
null -> LoadingState()
|
||||
false -> AddSourceRoute(onSaved = {}, onBack = null)
|
||||
true -> MainNavigation()
|
||||
null -> {
|
||||
LoadingState()
|
||||
}
|
||||
|
||||
false -> {
|
||||
AddSourceRoute(onSaved = {}, onBack = null)
|
||||
}
|
||||
|
||||
true -> {
|
||||
val playbackController = koinInject<PlaybackController>()
|
||||
LaunchedEffect(Unit) { playbackController.connect() }
|
||||
val playbackReady by playbackController.ready.collectAsStateWithLifecycle()
|
||||
if (playbackReady) MainNavigation() else LoadingState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +227,6 @@ private fun MainNavigation() {
|
||||
else -> repeat(pops) { navController.popBackStack() }
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) { playbackController.connect() }
|
||||
LaunchedEffect(Unit) { if (nowPlayingOpen) nowPlayingProgress = 1f }
|
||||
|
||||
val playerVisible = playback.item != null
|
||||
@@ -236,7 +243,7 @@ private fun MainNavigation() {
|
||||
val showMiniPlayer = playerVisible && !keyboardUp
|
||||
val bottomInset by animateDpAsState(
|
||||
targetValue = if (showMiniPlayer) 0.dp else navBarInset,
|
||||
animationSpec = if (playback.restored) snap() else tween(MINI_PLAYER_ANIM_MS),
|
||||
animationSpec = tween(MINI_PLAYER_ANIM_MS),
|
||||
label = "libraryBottomInset",
|
||||
)
|
||||
|
||||
@@ -379,18 +386,8 @@ private fun MainNavigation() {
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = showMiniPlayer,
|
||||
enter =
|
||||
if (playback.restored) {
|
||||
EnterTransition.None
|
||||
} else {
|
||||
expandVertically(tween(MINI_PLAYER_ANIM_MS), expandFrom = Alignment.Bottom)
|
||||
},
|
||||
exit =
|
||||
if (playback.restored) {
|
||||
ExitTransition.None
|
||||
} else {
|
||||
shrinkVertically(tween(MINI_PLAYER_ANIM_MS), shrinkTowards = Alignment.Bottom)
|
||||
},
|
||||
enter = expandVertically(tween(MINI_PLAYER_ANIM_MS), expandFrom = Alignment.Bottom),
|
||||
exit = shrinkVertically(tween(MINI_PLAYER_ANIM_MS), shrinkTowards = Alignment.Bottom),
|
||||
) {
|
||||
val positionMs by playbackController.positionMs.collectAsStateWithLifecycle()
|
||||
val miniArt = playbackController.coverArt(playback.item, thumbnail = true)
|
||||
|
||||
@@ -28,6 +28,7 @@ import okhttp3.OkHttpClient
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@@ -161,17 +162,16 @@ class PlaybackControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun restoringMarksTheStateUntilPlaybackStarts() {
|
||||
fun theReadyStateResolvesWithTheRestoredItemAlreadyInPlace() {
|
||||
seedAlbum(5, sourceId = 1)
|
||||
runBlocking { queues.replace(listOf(queues.albumEntry(1, "al1"))) }
|
||||
|
||||
assertFalse(controller.ready.value)
|
||||
|
||||
controller.connect()
|
||||
await { handle.items.isNotEmpty() }
|
||||
await { controller.ready.value }
|
||||
|
||||
assertTrue(controller.state.value.restored)
|
||||
|
||||
controller.playAlbum(1, "al1", 0)
|
||||
await { !controller.state.value.restored }
|
||||
assertNotNull("the mini player must be drawable once ready", controller.state.value.item)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user