From e48f108eeeb137e76eb872413bf18f713f7e58ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Fri, 26 Apr 2024 21:24:33 +0200 Subject: [PATCH] Allow Slider seeking player --- .../jetcaster/ui/player/PlayerScreen.kt | 80 +++++++++++++++---- .../jetcaster/ui/player/PlayerViewModel.kt | 8 ++ .../jetcaster/core/player/EpisodePlayer.kt | 10 +++ .../core/player/MockEpisodePlayer.kt | 11 +++ 4 files changed, 94 insertions(+), 15 deletions(-) diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt index 26904fbe1..5b93d1800 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt @@ -67,8 +67,11 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -107,8 +110,8 @@ import com.example.jetcaster.util.verticalGradientScrim import com.google.accompanist.adaptive.HorizontalTwoPaneStrategy import com.google.accompanist.adaptive.TwoPane import com.google.accompanist.adaptive.VerticalTwoPaneStrategy -import java.time.Duration import kotlinx.coroutines.launch +import java.time.Duration /** * Stateful version of the Podcast player @@ -130,6 +133,8 @@ fun PlayerScreen( onPausePress = viewModel::onPause, onAdvanceBy = viewModel::onAdvanceBy, onRewindBy = viewModel::onRewindBy, + onSeekingStarted = viewModel::onSeekingStarted, + onSeekingFinished = viewModel::onSeekingFinished, onStop = viewModel::onStop, onNext = viewModel::onNext, onPrevious = viewModel::onPrevious, @@ -150,6 +155,8 @@ private fun PlayerScreen( onPausePress: () -> Unit, onAdvanceBy: (Duration) -> Unit, onRewindBy: (Duration) -> Unit, + onSeekingStarted: () -> Unit, + onSeekingFinished: (Duration) -> Unit, onStop: () -> Unit, onNext: () -> Unit, onPrevious: () -> Unit, @@ -181,6 +188,8 @@ private fun PlayerScreen( onPausePress = onPausePress, onAdvanceBy = onAdvanceBy, onRewindBy = onRewindBy, + onSeekingStarted = onSeekingStarted, + onSeekingFinished = onSeekingFinished, onNext = onNext, onPrevious = onPrevious, onAddToQueue = { @@ -219,6 +228,8 @@ fun PlayerContentWithBackground( onPausePress: () -> Unit, onAdvanceBy: (Duration) -> Unit, onRewindBy: (Duration) -> Unit, + onSeekingStarted: () -> Unit, + onSeekingFinished: (Duration) -> Unit, onNext: () -> Unit, onPrevious: () -> Unit, onAddToQueue: () -> Unit, @@ -238,6 +249,8 @@ fun PlayerContentWithBackground( onPausePress = onPausePress, onAdvanceBy = onAdvanceBy, onRewindBy = onRewindBy, + onSeekingStarted = onSeekingStarted, + onSeekingFinished = onSeekingFinished, onNext = onNext, onPrevious = onPrevious, onAddToQueue = onAddToQueue, @@ -255,6 +268,8 @@ fun PlayerContent( onPausePress: () -> Unit, onAdvanceBy: (Duration) -> Unit, onRewindBy: (Duration) -> Unit, + onSeekingStarted: () -> Unit, + onSeekingFinished: (Duration) -> Unit, onNext: () -> Unit, onPrevious: () -> Unit, onAddToQueue: () -> Unit, @@ -275,10 +290,10 @@ fun PlayerContent( // or we have an impactful horizontal fold. Otherwise, we'll use a horizontal strategy. val usingVerticalStrategy = isTableTopPosture(foldingFeature) || - ( - isSeparatingPosture(foldingFeature) && - foldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL - ) + ( + isSeparatingPosture(foldingFeature) && + foldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL + ) if (usingVerticalStrategy) { TwoPane( @@ -295,6 +310,8 @@ fun PlayerContent( onPausePress = onPausePress, onAdvanceBy = onAdvanceBy, onRewindBy = onRewindBy, + onSeekingStarted = onSeekingStarted, + onSeekingFinished = onSeekingFinished, onNext = onNext, onPrevious = onPrevious, onAddToQueue = onAddToQueue, @@ -331,6 +348,8 @@ fun PlayerContent( onPausePress = onPausePress, onAdvanceBy = onAdvanceBy, onRewindBy = onRewindBy, + onSeekingStarted = onSeekingStarted, + onSeeking = onSeekingFinished, onNext = onNext, onPrevious = onPrevious, ) @@ -348,6 +367,8 @@ fun PlayerContent( onPausePress = onPausePress, onAdvanceBy = onAdvanceBy, onRewindBy = onRewindBy, + onSeekingStarted = onSeekingStarted, + onSeeking = onSeekingFinished, onNext = onNext, onPrevious = onPrevious, onAddToQueue = onAddToQueue, @@ -367,6 +388,8 @@ private fun PlayerContentRegular( onPausePress: () -> Unit, onAdvanceBy: (Duration) -> Unit, onRewindBy: (Duration) -> Unit, + onSeekingStarted: () -> Unit, + onSeeking: (Duration) -> Unit, onNext: () -> Unit, onPrevious: () -> Unit, onAddToQueue: () -> Unit, @@ -407,7 +430,9 @@ private fun PlayerContentRegular( ) { PlayerSlider( timeElapsed = playerEpisode.timeElapsed, - episodeDuration = currentEpisode.duration + episodeDuration = currentEpisode.duration, + onSeekingStarted = onSeekingStarted, + onSeekingFinished = onSeeking ) PlayerButtons( hasNext = playerEpisode.queue.isNotEmpty(), @@ -467,6 +492,8 @@ private fun PlayerContentTableTopBottom( onPausePress: () -> Unit, onAdvanceBy: (Duration) -> Unit, onRewindBy: (Duration) -> Unit, + onSeekingStarted: () -> Unit, + onSeekingFinished: (Duration) -> Unit, onNext: () -> Unit, onPrevious: () -> Unit, onAddToQueue: () -> Unit, @@ -513,7 +540,9 @@ private fun PlayerContentTableTopBottom( ) PlayerSlider( timeElapsed = episodePlayerState.timeElapsed, - episodeDuration = episode.duration + episodeDuration = episode.duration, + onSeekingStarted = onSeekingStarted, + onSeekingFinished = onSeekingFinished ) } } @@ -556,6 +585,8 @@ private fun PlayerContentBookEnd( onPausePress: () -> Unit, onAdvanceBy: (Duration) -> Unit, onRewindBy: (Duration) -> Unit, + onSeekingStarted: () -> Unit, + onSeeking: (Duration) -> Unit, onNext: () -> Unit, onPrevious: () -> Unit, modifier: Modifier = Modifier @@ -577,7 +608,9 @@ private fun PlayerContentBookEnd( ) PlayerSlider( timeElapsed = episodePlayerState.timeElapsed, - episodeDuration = episode.duration + episodeDuration = episode.duration, + onSeekingStarted = onSeekingStarted, + onSeekingFinished = onSeeking, ) PlayerButtons( hasNext = episodePlayerState.queue.isNotEmpty(), @@ -703,21 +736,36 @@ fun Duration.formatString(): String { } @Composable -private fun PlayerSlider(timeElapsed: Duration?, episodeDuration: Duration?) { - Column(Modifier.fillMaxWidth()) { +private fun PlayerSlider( + timeElapsed: Duration, + episodeDuration: Duration?, + onSeekingStarted: () -> Unit, + onSeekingFinished: (newElapsed: Duration) -> Unit, +) { + Column( + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + var sliderValue by remember(timeElapsed) { mutableStateOf(timeElapsed) } + val maxRange = (episodeDuration?.toSeconds() ?: 0).toFloat() + Row(Modifier.fillMaxWidth()) { Text( - text = "${timeElapsed?.formatString()} • ${episodeDuration?.formatString()}", + text = "${sliderValue.formatString()} • ${episodeDuration?.formatString()}", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) } - val sliderValue = (timeElapsed?.toSeconds() ?: 0).toFloat() - val maxRange = (episodeDuration?.toSeconds() ?: 0).toFloat() + Slider( - value = sliderValue, + value = sliderValue.seconds.toFloat(), valueRange = 0f..maxRange, - onValueChange = { } + onValueChange = { + onSeekingStarted() + sliderValue = Duration.ofSeconds(it.toLong()) + }, + onValueChangeFinished = { onSeekingFinished(sliderValue) } ) } } @@ -913,6 +961,8 @@ fun PlayerScreenPreview() { onPausePress = {}, onAdvanceBy = {}, onRewindBy = {}, + onSeekingStarted = {}, + onSeekingFinished = {}, onStop = {}, onNext = {}, onPrevious = {}, diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt index 9e18c8602..e19861fed 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt @@ -100,6 +100,14 @@ class PlayerViewModel @Inject constructor( episodePlayer.rewindBy(duration) } + fun onSeekingStarted() { + episodePlayer.onSeekingStarted() + } + + fun onSeekingFinished(duration: Duration) { + episodePlayer.onSeekingFinished(duration) + } + fun onAddToQueue() { uiState.episodePlayerState.currentEpisode?.let { episodePlayer.addToQueue(it) diff --git a/Jetcaster/core/src/main/java/com/example/jetcaster/core/player/EpisodePlayer.kt b/Jetcaster/core/src/main/java/com/example/jetcaster/core/player/EpisodePlayer.kt index cbb858a11..9c04f8098 100644 --- a/Jetcaster/core/src/main/java/com/example/jetcaster/core/player/EpisodePlayer.kt +++ b/Jetcaster/core/src/main/java/com/example/jetcaster/core/player/EpisodePlayer.kt @@ -103,6 +103,16 @@ interface EpisodePlayer { */ fun rewindBy(duration: Duration) + /** + * Signal that user started seeking. + */ + fun onSeekingStarted() + + /** + * Seeks to a given time interval specified in [duration]. + */ + fun onSeekingFinished(duration: Duration) + /** * Increases the speed of Player playback by a given time specified in [duration]. */ diff --git a/Jetcaster/core/src/main/java/com/example/jetcaster/core/player/MockEpisodePlayer.kt b/Jetcaster/core/src/main/java/com/example/jetcaster/core/player/MockEpisodePlayer.kt index 4c65a9039..61dcb1e62 100644 --- a/Jetcaster/core/src/main/java/com/example/jetcaster/core/player/MockEpisodePlayer.kt +++ b/Jetcaster/core/src/main/java/com/example/jetcaster/core/player/MockEpisodePlayer.kt @@ -173,6 +173,17 @@ class MockEpisodePlayer( } } + override fun onSeekingStarted() { + // Need to pause the player so that it doesn't compete with timeline progression. + pause() + } + + override fun onSeekingFinished(duration: Duration) { + val currentEpisodeDuration = _currentEpisode.value?.duration ?: return + timeElapsed.update { duration.coerceIn(Duration.ZERO, currentEpisodeDuration) } + play() + } + override fun increaseSpeed(speed: Duration) { _playerSpeed.value += speed }