mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 16:11:20 +02:00
Better next up layout
This commit is contained in:
parent
628771211d
commit
a963074f07
4 changed files with 125 additions and 43 deletions
|
|
@ -31,12 +31,12 @@ import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
||||||
@Composable
|
@Composable
|
||||||
fun BannerCard(
|
fun BannerCard(
|
||||||
imageUrl: String?,
|
imageUrl: String?,
|
||||||
cornerText: String?,
|
|
||||||
played: Boolean,
|
|
||||||
playPercent: Double,
|
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
cornerText: String? = null,
|
||||||
|
played: Boolean = false,
|
||||||
|
playPercent: Double = 0.0,
|
||||||
cardHeight: Dp = 140.dp * .85f,
|
cardHeight: Dp = 140.dp * .85f,
|
||||||
aspectRatio: Float = 16f / 9,
|
aspectRatio: Float = 16f / 9,
|
||||||
interactionSource: MutableInteractionSource? = null,
|
interactionSource: MutableInteractionSource? = null,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
package com.github.damontecres.dolphin.ui.playback
|
package com.github.damontecres.dolphin.ui.playback
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
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.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.remember
|
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.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
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.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import coil3.compose.AsyncImage
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
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
|
import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NextUpEpisode(
|
fun NextUpEpisode(
|
||||||
ep: BaseItem,
|
title: String?,
|
||||||
|
description: String?,
|
||||||
|
imageUrl: String?,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
aspectRatio: Float = 16f / 9,
|
||||||
) {
|
) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||||
Card(
|
Box(
|
||||||
onClick = onClick,
|
modifier = modifier,
|
||||||
modifier = modifier.focusRequester(focusRequester),
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = Modifier.padding(8.dp),
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(8.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Up Next...",
|
text = "Up Next...",
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier,
|
modifier = Modifier.padding(8.dp),
|
||||||
) {
|
) {
|
||||||
Box {
|
Box {
|
||||||
AsyncImage(
|
BannerCard(
|
||||||
model = ep.imageUrl,
|
imageUrl = imageUrl,
|
||||||
contentDescription = ep.name,
|
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(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = Modifier.fillMaxHeight(),
|
modifier = Modifier.fillMaxHeight(),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = ep.data.name ?: "",
|
text = title ?: "",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = ep.data.overview ?: "",
|
text = description ?: "",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier,
|
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),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,13 @@ import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.systemBars
|
import androidx.compose.foundation.layout.systemBars
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
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.rememberPresentationState
|
||||||
import androidx.media3.ui.compose.state.rememberPreviousButtonState
|
import androidx.media3.ui.compose.state.rememberPreviousButtonState
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.dolphin.ui.OneTimeLaunchedEffect
|
import com.github.damontecres.dolphin.ui.OneTimeLaunchedEffect
|
||||||
import com.github.damontecres.dolphin.ui.components.LoadingPage
|
import com.github.damontecres.dolphin.ui.components.LoadingPage
|
||||||
|
|
@ -320,14 +323,22 @@ fun PlaybackContent(
|
||||||
) {
|
) {
|
||||||
nextUp?.let {
|
nextUp?.let {
|
||||||
NextUpEpisode(
|
NextUpEpisode(
|
||||||
ep = it,
|
title = it.name,
|
||||||
|
description = it.data.overview,
|
||||||
|
imageUrl = it.imageUrl,
|
||||||
|
aspectRatio = it.data.primaryImageAspectRatio?.toFloat() ?: (16f / 9),
|
||||||
onClick = { viewModel.playUpNextEpisode() },
|
onClick = { viewModel.playUpNextEpisode() },
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(16.dp)
|
.padding(8.dp)
|
||||||
.height(128.dp)
|
// .height(128.dp)
|
||||||
.fillMaxWidth(.4f)
|
.fillMaxHeight(.3f)
|
||||||
.align(Alignment.BottomCenter),
|
.fillMaxWidth(.5f)
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,6 @@ import com.github.damontecres.dolphin.util.subtitleMimeTypes
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
|
@ -54,9 +52,6 @@ import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.time.Duration
|
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 {
|
enum class TranscodeType {
|
||||||
DIRECT_PLAY,
|
DIRECT_PLAY,
|
||||||
|
|
@ -451,23 +446,40 @@ class PlaybackViewModel
|
||||||
Timber.v("Current episode is $currentEpisodeIndex of ${episodes.size}")
|
Timber.v("Current episode is $currentEpisodeIndex of ${episodes.size}")
|
||||||
val nextIndex = currentEpisodeIndex + 1
|
val nextIndex = currentEpisodeIndex + 1
|
||||||
if (nextIndex < episodes.size) {
|
if (nextIndex < episodes.size) {
|
||||||
|
val listener =
|
||||||
|
object : Player.Listener {
|
||||||
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
|
if (playbackState == Player.STATE_ENDED) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
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)
|
val nextItem = episodes.getBlocking(nextIndex)
|
||||||
Timber.v("Setting next up episode to ${nextItem?.id}")
|
Timber.v("Setting next up episode to ${nextItem?.id}")
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
nextUpEpisode.value = nextItem
|
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
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue