Better next up layout

This commit is contained in:
Damontecres 2025-10-06 13:11:04 -04:00
parent 628771211d
commit a963074f07
No known key found for this signature in database
4 changed files with 125 additions and 43 deletions

View file

@ -31,12 +31,12 @@ import com.github.damontecres.dolphin.ui.isNotNullOrBlank
@Composable
fun BannerCard(
imageUrl: String?,
cornerText: String?,
played: Boolean,
playPercent: Double,
onClick: () -> Unit,
onLongClick: () -> Unit,
modifier: Modifier = Modifier,
cornerText: String? = null,
played: Boolean = false,
playPercent: Double = 0.0,
cardHeight: Dp = 140.dp * .85f,
aspectRatio: Float = 16f / 9,
interactionSource: MutableInteractionSource? = null,

View file

@ -1,12 +1,20 @@
package com.github.damontecres.dolphin.ui.playback
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
@ -17,60 +25,86 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.tv.material3.Card
import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import coil3.compose.AsyncImage
import com.github.damontecres.dolphin.data.model.BaseItem
import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.dolphin.ui.PreviewTvSpec
import com.github.damontecres.dolphin.ui.cards.BannerCard
import com.github.damontecres.dolphin.ui.theme.DolphinTheme
import com.github.damontecres.dolphin.ui.tryRequestFocus
@Composable
fun NextUpEpisode(
ep: BaseItem,
title: String?,
description: String?,
imageUrl: String?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
aspectRatio: Float = 16f / 9,
) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
Card(
onClick = onClick,
modifier = modifier.focusRequester(focusRequester),
Box(
modifier = modifier,
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(8.dp),
modifier =
Modifier
.fillMaxSize()
.padding(8.dp),
) {
Text(
text = "Up Next...",
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier,
modifier = Modifier.padding(8.dp),
) {
Box {
AsyncImage(
model = ep.imageUrl,
contentDescription = ep.name,
BannerCard(
imageUrl = imageUrl,
onClick = onClick,
onLongClick = {},
cardHeight = 100.dp,
aspectRatio = aspectRatio,
modifier =
Modifier
.focusRequester(focusRequester)
.align(Alignment.Center),
)
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = null,
tint = MaterialTheme.colorScheme.border,
modifier =
Modifier
.size(60.dp)
.align(Alignment.Center),
)
}
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxHeight(),
) {
Text(
text = ep.data.name ?: "",
text = title ?: "",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier,
)
Text(
text = ep.data.overview ?: "",
text = description ?: "",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis,
modifier = Modifier,
)
@ -79,3 +113,28 @@ fun NextUpEpisode(
}
}
}
@PreviewTvSpec
@Composable
fun NextUpEpisodePreview() {
DolphinTheme(true) {
NextUpEpisode(
title = "Episode Title",
description = "This is the description of the episode. It might be long.",
imageUrl = "",
onClick = {},
aspectRatio = 4f / 3,
modifier =
Modifier
.padding(16.dp)
.height(200.dp)
.width(400.dp)
// .fillMaxWidth(.4f)
// .align(Alignment.BottomCenter)
.background(
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
shape = RoundedCornerShape(8.dp),
),
)
}
}

View file

@ -11,11 +11,13 @@ import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@ -53,6 +55,7 @@ import androidx.media3.ui.compose.state.rememberPlayPauseButtonState
import androidx.media3.ui.compose.state.rememberPresentationState
import androidx.media3.ui.compose.state.rememberPreviousButtonState
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.dolphin.ui.components.LoadingPage
@ -320,14 +323,22 @@ fun PlaybackContent(
) {
nextUp?.let {
NextUpEpisode(
ep = it,
title = it.name,
description = it.data.overview,
imageUrl = it.imageUrl,
aspectRatio = it.data.primaryImageAspectRatio?.toFloat() ?: (16f / 9),
onClick = { viewModel.playUpNextEpisode() },
modifier =
Modifier
.padding(16.dp)
.height(128.dp)
.fillMaxWidth(.4f)
.align(Alignment.BottomCenter),
.padding(8.dp)
// .height(128.dp)
.fillMaxHeight(.3f)
.fillMaxWidth(.5f)
.align(Alignment.BottomCenter)
.background(
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
shape = RoundedCornerShape(8.dp),
),
)
}
}

View file

@ -30,8 +30,6 @@ import com.github.damontecres.dolphin.util.subtitleMimeTypes
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient
@ -54,9 +52,6 @@ import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
enum class TranscodeType {
DIRECT_PLAY,
@ -451,23 +446,40 @@ class PlaybackViewModel
Timber.v("Current episode is $currentEpisodeIndex of ${episodes.size}")
val nextIndex = currentEpisodeIndex + 1
if (nextIndex < episodes.size) {
val listener =
object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == Player.STATE_ENDED) {
viewModelScope.launch(Dispatchers.IO) {
while (this.isActive) {
delay(5.seconds)
val remaining =
withContext(Dispatchers.Main) {
(player.duration - player.currentPosition).milliseconds
}
if (remaining < 2.minutes) { // TODO time & preference
val nextItem = episodes.getBlocking(nextIndex)
Timber.v("Setting next up episode to ${nextItem?.id}")
withContext(Dispatchers.Main) {
nextUpEpisode.value = nextItem
}
break
}
player.removeListener(this)
}
}
}
player.addListener(listener)
// viewModelScope.launch(Dispatchers.IO) {
// while (this.isActive) {
// delay(5.seconds)
// val remaining =
// withContext(Dispatchers.Main) {
// (player.duration - player.currentPosition).milliseconds
// }
// if (remaining < 2.minutes) { // TODO time & preference
// val nextItem = episodes.getBlocking(nextIndex)
// Timber.v("Setting next up episode to ${nextItem?.id}")
// withContext(Dispatchers.Main) {
// nextUpEpisode.value = nextItem
// }
// break
// }
// }
// }
}
}
}