mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-24 03:36:51 +00:00
feat(player): add next episode overlay that appears 20s before episode end
Show an "Up Next" card with the next episode thumbnail, play overlay, and title when within 20 seconds of the current episode ending. Tapping it immediately starts the next episode from the playlist queue. - Add nextEpisode field to PlayerUiState derived from queue - Create NextEpisodeOverlay for mobile with artwork + title - Create TvNextEpisodeOverlay for TV with D-pad focus support - Integrate into both PlayerScreen and TvPlayerScreen - Auto-focus overlay on TV when it appears
This commit is contained in:
@@ -57,7 +57,9 @@ dependencies {
|
|||||||
implementation(project(":core"))
|
implementation(project(":core"))
|
||||||
implementation(project(":core-model"))
|
implementation(project(":core-model"))
|
||||||
implementation(project(":core-ui"))
|
implementation(project(":core-ui"))
|
||||||
|
implementation(libs.androidx.compose.animation)
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.androidx.foundation)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import hu.bbara.purefin.core.player.viewmodel.ControlsAutoHideBlocker
|
|||||||
import hu.bbara.purefin.core.player.viewmodel.PlayerViewModel
|
import hu.bbara.purefin.core.player.viewmodel.PlayerViewModel
|
||||||
import hu.bbara.purefin.ui.screen.player.components.PlayerSeekBarTrack
|
import hu.bbara.purefin.ui.screen.player.components.PlayerSeekBarTrack
|
||||||
import hu.bbara.purefin.ui.screen.player.components.TvIconButton
|
import hu.bbara.purefin.ui.screen.player.components.TvIconButton
|
||||||
|
import hu.bbara.purefin.ui.screen.player.components.TvNextEpisodeOverlay
|
||||||
import hu.bbara.purefin.ui.screen.player.components.TvPlayerTimeRow
|
import hu.bbara.purefin.ui.screen.player.components.TvPlayerTimeRow
|
||||||
import hu.bbara.purefin.ui.screen.player.components.TvPlayerControlsOverlay
|
import hu.bbara.purefin.ui.screen.player.components.TvPlayerControlsOverlay
|
||||||
import hu.bbara.purefin.ui.screen.player.components.TvPlayerLoadingErrorEndCard
|
import hu.bbara.purefin.ui.screen.player.components.TvPlayerLoadingErrorEndCard
|
||||||
@@ -104,6 +105,7 @@ fun TvPlayerScreen(
|
|||||||
val audioButtonFocusRequester = remember { FocusRequester() }
|
val audioButtonFocusRequester = remember { FocusRequester() }
|
||||||
val subtitlesButtonFocusRequester = remember { FocusRequester() }
|
val subtitlesButtonFocusRequester = remember { FocusRequester() }
|
||||||
val skipButtonFocusRequester = remember { FocusRequester() }
|
val skipButtonFocusRequester = remember { FocusRequester() }
|
||||||
|
val nextEpisodeFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
LifecycleEventEffect(Lifecycle.Event.ON_STOP) {
|
LifecycleEventEffect(Lifecycle.Event.ON_STOP) {
|
||||||
viewModel.pausePlayback()
|
viewModel.pausePlayback()
|
||||||
@@ -199,10 +201,20 @@ fun TvPlayerScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val playerControlsVisible =
|
||||||
|
controlsVisible || isPlaylistExpanded || trackPanelType != null || uiState.isEnded || uiState.error != null
|
||||||
|
|
||||||
|
val showNextEpisodeOverlay = !playerControlsVisible
|
||||||
|
&& uiState.nextEpisode != null
|
||||||
|
&& uiState.durationMs > 0L
|
||||||
|
&& (uiState.durationMs - uiState.positionMs) <= 60_000L
|
||||||
|
&& !uiState.isEnded
|
||||||
|
|
||||||
LaunchedEffect(
|
LaunchedEffect(
|
||||||
controlsVisible,
|
controlsVisible,
|
||||||
controlsAutoHideBlocked,
|
controlsAutoHideBlocked,
|
||||||
uiState.activeSkippableSegmentEndMs
|
uiState.activeSkippableSegmentEndMs,
|
||||||
|
showNextEpisodeOverlay
|
||||||
) {
|
) {
|
||||||
if (controlsAutoHideBlocked) return@LaunchedEffect
|
if (controlsAutoHideBlocked) return@LaunchedEffect
|
||||||
if (controlsVisible) {
|
if (controlsVisible) {
|
||||||
@@ -212,6 +224,10 @@ fun TvPlayerScreen(
|
|||||||
skipButtonFocusRequester.requestFocus()
|
skipButtonFocusRequester.requestFocus()
|
||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
|
if (showNextEpisodeOverlay) {
|
||||||
|
nextEpisodeFocusRequester.requestFocus()
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,8 +262,6 @@ fun TvPlayerScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val playerControlsVisible =
|
|
||||||
controlsVisible || isPlaylistExpanded || trackPanelType != null || uiState.isEnded || uiState.error != null
|
|
||||||
val subtitleBottomPaddingFraction =
|
val subtitleBottomPaddingFraction =
|
||||||
if (playerControlsVisible) {
|
if (playerControlsVisible) {
|
||||||
CONTROLS_VISIBLE_SUBTITLE_BOTTOM_PADDING_FRACTION
|
CONTROLS_VISIBLE_SUBTITLE_BOTTOM_PADDING_FRACTION
|
||||||
@@ -364,6 +378,27 @@ fun TvPlayerScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = showNextEpisodeOverlay,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(
|
||||||
|
end = 32.dp,
|
||||||
|
bottom = 140.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
uiState.nextEpisode?.let { nextEpisode ->
|
||||||
|
TvNextEpisodeOverlay(
|
||||||
|
nextEpisode = nextEpisode,
|
||||||
|
onClick = { nextAndShowControls() },
|
||||||
|
onUp = { showTvControls() },
|
||||||
|
modifier = Modifier.focusRequester(nextEpisodeFocusRequester)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = !playerControlsVisible && hiddenSeekPreviewPositionMs != null,
|
visible = !playerControlsVisible && hiddenSeekPreviewPositionMs != null,
|
||||||
enter = fadeIn(),
|
enter = fadeIn(),
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package hu.bbara.purefin.ui.screen.player.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
|
import androidx.compose.ui.input.key.key
|
||||||
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
|
import androidx.compose.ui.input.key.type
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import hu.bbara.purefin.core.player.model.PlaylistElementUiModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TV-optimized overlay button that appears near the end of a media item to let the user
|
||||||
|
* quickly jump to the next episode. Shows the next episode's artwork and title.
|
||||||
|
* Supports D-pad navigation and focus animations.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun TvNextEpisodeOverlay(
|
||||||
|
nextEpisode: PlaylistElementUiModel,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onUp: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
var isFocused by remember { mutableStateOf(false) }
|
||||||
|
val scale by animateFloatAsState(
|
||||||
|
targetValue = if (isFocused) 1.05f else 1f,
|
||||||
|
label = "nextEpisodeScale"
|
||||||
|
)
|
||||||
|
val borderColor by animateColorAsState(
|
||||||
|
targetValue = if (isFocused) scheme.primary else Color.Transparent,
|
||||||
|
label = "nextEpisodeBorder"
|
||||||
|
)
|
||||||
|
val backgroundColor by animateColorAsState(
|
||||||
|
targetValue = if (isFocused) {
|
||||||
|
scheme.surfaceContainerHigh.copy(alpha = 0.96f)
|
||||||
|
} else {
|
||||||
|
Color.Black.copy(alpha = 0.85f)
|
||||||
|
},
|
||||||
|
label = "nextEpisodeBackground"
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.width(280.dp)
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
}
|
||||||
|
.border(
|
||||||
|
width = if (isFocused) 3.dp else 0.dp,
|
||||||
|
color = borderColor,
|
||||||
|
shape = RoundedCornerShape(20.dp)
|
||||||
|
)
|
||||||
|
.clip(RoundedCornerShape(20.dp))
|
||||||
|
.background(backgroundColor)
|
||||||
|
.focusable()
|
||||||
|
.onFocusChanged { isFocused = it.isFocused }
|
||||||
|
.onPreviewKeyEvent { event ->
|
||||||
|
if (event.type == KeyEventType.KeyDown && event.key == Key.DirectionUp) {
|
||||||
|
onUp()
|
||||||
|
true
|
||||||
|
} else false
|
||||||
|
}
|
||||||
|
.clickable { onClick() },
|
||||||
|
verticalArrangement = Arrangement.spacedBy(0.dp)
|
||||||
|
) {
|
||||||
|
// "Up Next" label
|
||||||
|
Text(
|
||||||
|
text = "Up Next",
|
||||||
|
color = scheme.secondary,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.padding(start = 16.dp, top = 12.dp, end = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
|
||||||
|
// Thumbnail with play button overlay
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.aspectRatio(16f / 9f)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = nextEpisode.artworkUrl?.takeIf { it.isNotEmpty() },
|
||||||
|
contentDescription = nextEpisode.title,
|
||||||
|
modifier = Modifier.matchParentSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
|
||||||
|
// Dark gradient overlay for readability
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Black.copy(alpha = 0.1f),
|
||||||
|
Color.Black.copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Episode title
|
||||||
|
Text(
|
||||||
|
text = nextEpisode.title,
|
||||||
|
color = scheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 14.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal const val TvPlayerNextEpisodeOverlayTag = "tv_player_next_episode_overlay"
|
||||||
@@ -51,6 +51,7 @@ import hu.bbara.purefin.ui.screen.player.components.PlayerControlsOverlay
|
|||||||
import hu.bbara.purefin.ui.screen.player.components.PlayerGesturesLayer
|
import hu.bbara.purefin.ui.screen.player.components.PlayerGesturesLayer
|
||||||
import hu.bbara.purefin.ui.screen.player.components.PlayerLoadingErrorEndCard
|
import hu.bbara.purefin.ui.screen.player.components.PlayerLoadingErrorEndCard
|
||||||
import hu.bbara.purefin.ui.screen.player.components.PlayerQueuePanel
|
import hu.bbara.purefin.ui.screen.player.components.PlayerQueuePanel
|
||||||
|
import hu.bbara.purefin.ui.screen.player.components.NextEpisodeOverlay
|
||||||
import hu.bbara.purefin.ui.screen.player.components.PlayerSeekBarTrack
|
import hu.bbara.purefin.ui.screen.player.components.PlayerSeekBarTrack
|
||||||
import hu.bbara.purefin.ui.screen.player.components.PlayerTimeRow
|
import hu.bbara.purefin.ui.screen.player.components.PlayerTimeRow
|
||||||
import hu.bbara.purefin.ui.screen.player.components.SkipSegmentButton
|
import hu.bbara.purefin.ui.screen.player.components.SkipSegmentButton
|
||||||
@@ -261,6 +262,30 @@ fun PlayerScreen(
|
|||||||
SkipSegmentButton(onClick = { viewModel.skipActiveSegment() })
|
SkipSegmentButton(onClick = { viewModel.skipActiveSegment() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val showNextEpisodeOverlay = !playerControlsVisible
|
||||||
|
&& uiState.nextEpisode != null
|
||||||
|
&& uiState.durationMs > 0L
|
||||||
|
&& (uiState.durationMs - uiState.positionMs) <= 60_000L
|
||||||
|
&& !uiState.isEnded
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = showNextEpisodeOverlay,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(
|
||||||
|
end = 24.dp,
|
||||||
|
bottom = 120.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
uiState.nextEpisode?.let { nextEpisode ->
|
||||||
|
NextEpisodeOverlay(
|
||||||
|
nextEpisode = nextEpisode,
|
||||||
|
onClick = { viewModel.next() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PlayerLoadingErrorEndCard(
|
PlayerLoadingErrorEndCard(
|
||||||
modifier = Modifier.align(Alignment.Center),
|
modifier = Modifier.align(Alignment.Center),
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package hu.bbara.purefin.ui.screen.player.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import hu.bbara.purefin.core.player.model.PlaylistElementUiModel
|
||||||
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overlay button that appears near the end of a media item to let the user
|
||||||
|
* quickly jump to the next episode. Shows the next episode's artwork and title.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun NextEpisodeOverlay(
|
||||||
|
nextEpisode: PlaylistElementUiModel,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.width(200.dp)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(Color.Black.copy(alpha = 0.85f))
|
||||||
|
.clickable { onClick() },
|
||||||
|
verticalArrangement = Arrangement.spacedBy(0.dp)
|
||||||
|
) {
|
||||||
|
// "Up Next" label
|
||||||
|
Text(
|
||||||
|
text = "Up Next",
|
||||||
|
color = scheme.secondary,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.padding(start = 12.dp, top = 8.dp, end = 12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
// Thumbnail with play button overlay
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
.aspectRatio(16f / 9f)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
) {
|
||||||
|
PurefinAsyncImage(
|
||||||
|
model = nextEpisode.artworkUrl,
|
||||||
|
contentDescription = nextEpisode.title,
|
||||||
|
modifier = Modifier.matchParentSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
|
||||||
|
// Dark gradient overlay for readability
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Black.copy(alpha = 0.1f),
|
||||||
|
Color.Black.copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Episode title
|
||||||
|
Text(
|
||||||
|
text = nextEpisode.title,
|
||||||
|
color = scheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 10.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal const val PlayerNextEpisodeOverlayTag = "player_next_episode_overlay"
|
||||||
@@ -16,6 +16,7 @@ data class PlayerUiState(
|
|||||||
val chapters: List<TimedMarker> = emptyList(),
|
val chapters: List<TimedMarker> = emptyList(),
|
||||||
val ads: List<TimedMarker> = emptyList(),
|
val ads: List<TimedMarker> = emptyList(),
|
||||||
val queue: List<PlaylistElementUiModel> = emptyList(),
|
val queue: List<PlaylistElementUiModel> = emptyList(),
|
||||||
|
val nextEpisode: PlaylistElementUiModel? = null,
|
||||||
val audioTracks: List<TrackOption> = emptyList(),
|
val audioTracks: List<TrackOption> = emptyList(),
|
||||||
val textTracks: List<TrackOption> = emptyList(),
|
val textTracks: List<TrackOption> = emptyList(),
|
||||||
val qualityTracks: List<TrackOption> = emptyList(),
|
val qualityTracks: List<TrackOption> = emptyList(),
|
||||||
|
|||||||
@@ -139,9 +139,16 @@ class PlayerViewModel @Inject constructor(
|
|||||||
playableMedia.toPlaylistElementUiModel(currentPlayableMedia?.id)
|
playableMedia.toPlaylistElementUiModel(currentPlayableMedia?.id)
|
||||||
}
|
}
|
||||||
}.collect { queue ->
|
}.collect { queue ->
|
||||||
|
val currentIndex = queue.indexOfFirst { it.isCurrent }
|
||||||
|
val nextEpisode = if (currentIndex != -1 && currentIndex + 1 < queue.size) {
|
||||||
|
queue[currentIndex + 1]
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
_uiState.update { state ->
|
_uiState.update { state ->
|
||||||
state.copy(
|
state.copy(
|
||||||
queue = queue
|
queue = queue,
|
||||||
|
nextEpisode = nextEpisode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ timber = "5.0.1"
|
|||||||
firebaseCrashlytics = "20.0.5"
|
firebaseCrashlytics = "20.0.5"
|
||||||
googleGmsGoogleServices = "4.4.4"
|
googleGmsGoogleServices = "4.4.4"
|
||||||
googleFirebaseCrashlytics = "3.0.7"
|
googleFirebaseCrashlytics = "3.0.7"
|
||||||
|
foundationVersion = "1.11.2"
|
||||||
|
animation = "1.11.2"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -76,6 +78,8 @@ androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref =
|
|||||||
slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
|
slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
|
||||||
timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
|
timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
|
||||||
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics", version.ref = "firebaseCrashlytics" }
|
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics", version.ref = "firebaseCrashlytics" }
|
||||||
|
androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundationVersion" }
|
||||||
|
androidx-compose-animation = { group = "androidx.compose.animation", name = "animation", version.ref = "animation" }
|
||||||
|
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
|||||||
Reference in New Issue
Block a user