diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/BannerCard.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/BannerCard.kt index 2ac4ba39..e3494cc4 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/BannerCard.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/BannerCard.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/NextUpEpisode.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/NextUpEpisode.kt index a9063ab7..a50ac433 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/NextUpEpisode.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/NextUpEpisode.kt @@ -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), + ), + ) + } +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackContent.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackContent.kt index eaf37cda..b6561b8d 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackContent.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackContent.kt @@ -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), + ), ) } } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt index 942379fa..5571dd10 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt @@ -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) { - viewModelScope.launch(Dispatchers.IO) { - while (this.isActive) { - delay(5.seconds) - val remaining = - withContext(Dispatchers.Main) { - (player.duration - player.currentPosition).milliseconds + val listener = + object : Player.Listener { + override fun onPlaybackStateChanged(playbackState: Int) { + if (playbackState == Player.STATE_ENDED) { + viewModelScope.launch(Dispatchers.IO) { + val nextItem = episodes.getBlocking(nextIndex) + Timber.v("Setting next up episode to ${nextItem?.id}") + withContext(Dispatchers.Main) { + nextUpEpisode.value = nextItem + } + } + player.removeListener(this) } - 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.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 +// } +// } +// } } } }