mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Update movie details
This commit is contained in:
parent
6e42cd3c65
commit
bdfe143308
9 changed files with 514 additions and 200 deletions
|
|
@ -24,6 +24,7 @@ import org.jellyfin.sdk.model.api.BaseItemDto
|
|||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import timber.log.Timber
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
|
@ -186,3 +187,11 @@ val BaseItemDto.timeRemaining: Duration?
|
|||
fun Player.seekBack(amount: Duration) = seekTo((currentPosition - amount.inWholeMilliseconds).coerceAtLeast(0L))
|
||||
|
||||
fun Player.seekForward(amount: Duration) = seekTo((currentPosition + amount.inWholeMilliseconds).coerceAtMost(duration))
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T : Collection<*>, R> T.letNotEmpty(block: (T) -> R): R? {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
|
||||
}
|
||||
return if (this.isNotEmpty()) block(this) else null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.FocusState
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.focusRestorer
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
|
|
@ -26,6 +27,7 @@ import androidx.tv.material3.Icon
|
|||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.dolphin.R
|
||||
import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||
import kotlin.time.Duration
|
||||
|
||||
@Composable
|
||||
|
|
@ -109,3 +111,84 @@ fun PlayButtons(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExpandablePlayButtons(
|
||||
resumePosition: Duration,
|
||||
watched: Boolean,
|
||||
playOnClick: (position: Duration) -> Unit,
|
||||
watchOnClick: () -> Unit,
|
||||
moreOnClick: () -> Unit,
|
||||
buttonOnFocusChanged: (FocusState) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val firstFocus = remember { FocusRequester() }
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = PaddingValues(8.dp),
|
||||
modifier =
|
||||
modifier
|
||||
.focusGroup()
|
||||
.focusProperties {
|
||||
onEnter = {
|
||||
firstFocus.tryRequestFocus()
|
||||
}
|
||||
},
|
||||
) {
|
||||
if (resumePosition > Duration.ZERO) {
|
||||
item {
|
||||
// LaunchedEffect(Unit) { firstFocus.tryRequestFocus() }
|
||||
ExpandablePlayButton(
|
||||
R.string.resume,
|
||||
resumePosition,
|
||||
Icons.Default.PlayArrow,
|
||||
playOnClick,
|
||||
Modifier
|
||||
.onFocusChanged(buttonOnFocusChanged)
|
||||
.focusRequester(firstFocus),
|
||||
)
|
||||
}
|
||||
item {
|
||||
ExpandablePlayButton(
|
||||
R.string.restart,
|
||||
Duration.ZERO,
|
||||
Icons.Default.Refresh,
|
||||
playOnClick,
|
||||
Modifier.onFocusChanged(buttonOnFocusChanged),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
ExpandablePlayButton(
|
||||
R.string.play,
|
||||
Duration.ZERO,
|
||||
Icons.Default.PlayArrow,
|
||||
playOnClick,
|
||||
Modifier
|
||||
.onFocusChanged(buttonOnFocusChanged)
|
||||
.focusRequester(firstFocus),
|
||||
)
|
||||
}
|
||||
}
|
||||
// Watched button
|
||||
item {
|
||||
ExpandableFaButton(
|
||||
title = if (watched) R.string.mark_unwatched else R.string.mark_watched,
|
||||
iconStringRes = if (watched) R.string.fa_eye else R.string.fa_eye_slash,
|
||||
onClick = watchOnClick,
|
||||
modifier = Modifier.onFocusChanged(buttonOnFocusChanged),
|
||||
)
|
||||
}
|
||||
|
||||
// More button
|
||||
item {
|
||||
ExpandablePlayButton(
|
||||
R.string.more,
|
||||
Duration.ZERO,
|
||||
Icons.Default.MoreVert,
|
||||
{ moreOnClick.invoke() },
|
||||
Modifier.onFocusChanged(buttonOnFocusChanged),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,105 +0,0 @@
|
|||
package com.github.damontecres.dolphin.ui.detail
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.tv.material3.Button
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.dolphin.data.model.Video
|
||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||
import com.github.damontecres.dolphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.dolphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
||||
import com.github.damontecres.dolphin.util.LoadingState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@HiltViewModel
|
||||
class MovieViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
api: ApiClient,
|
||||
) : LoadingItemViewModel<Video>(api)
|
||||
|
||||
@Composable
|
||||
fun MovieDetails(
|
||||
preferences: UserPreferences,
|
||||
navigationManager: NavigationManager,
|
||||
destination: Destination.MediaItem,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: MovieViewModel = hiltViewModel(),
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.init(destination.itemId, destination.item)
|
||||
}
|
||||
val item by viewModel.item.observeAsState()
|
||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||
when (val state = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
LoadingState.Loading -> LoadingPage()
|
||||
LoadingState.Success -> {
|
||||
item?.let { item ->
|
||||
val dto = item.data
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(32.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
item {
|
||||
Text(text = item.name ?: "Unknown")
|
||||
}
|
||||
dto.overview?.let {
|
||||
item {
|
||||
Text(text = it)
|
||||
}
|
||||
}
|
||||
dto.userData?.playbackPositionTicks?.ticks?.let {
|
||||
if (it > 60.seconds) {
|
||||
item {
|
||||
Button(
|
||||
onClick = {
|
||||
navigationManager.navigateTo(
|
||||
Destination.Playback(
|
||||
item.id,
|
||||
it.inWholeMilliseconds,
|
||||
item,
|
||||
),
|
||||
)
|
||||
},
|
||||
) {
|
||||
Text(text = "Resume")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Button(
|
||||
onClick = {
|
||||
navigationManager.navigateTo(
|
||||
Destination.Playback(
|
||||
item.id,
|
||||
0L,
|
||||
item,
|
||||
),
|
||||
)
|
||||
},
|
||||
) {
|
||||
Text(text = "Play")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import com.github.damontecres.dolphin.data.model.Video
|
|||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||
import com.github.damontecres.dolphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.dolphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.dolphin.ui.detail.movie.MovieViewModel
|
||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
||||
import com.github.damontecres.dolphin.util.LoadingState
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
package com.github.damontecres.dolphin.ui.detail.movie
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import coil3.compose.AsyncImage
|
||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||
import com.github.damontecres.dolphin.data.model.Video
|
||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||
import com.github.damontecres.dolphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.dolphin.ui.components.ExpandablePlayButtons
|
||||
import com.github.damontecres.dolphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.dolphin.ui.detail.LoadingItemViewModel
|
||||
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
||||
import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.dolphin.util.ExceptionHandler
|
||||
import com.github.damontecres.dolphin.util.LoadingState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration
|
||||
|
||||
@HiltViewModel
|
||||
class MovieViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
api: ApiClient,
|
||||
) : LoadingItemViewModel<Video>(api)
|
||||
|
||||
@Composable
|
||||
fun MovieDetails(
|
||||
preferences: UserPreferences,
|
||||
navigationManager: NavigationManager,
|
||||
destination: Destination.MediaItem,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: MovieViewModel = hiltViewModel(),
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.init(destination.itemId, destination.item)
|
||||
}
|
||||
val item by viewModel.item.observeAsState()
|
||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||
when (val state = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
LoadingState.Loading -> LoadingPage()
|
||||
LoadingState.Success -> {
|
||||
item?.let {
|
||||
MovieDetailsContent(
|
||||
preferences,
|
||||
navigationManager,
|
||||
it,
|
||||
modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MovieDetailsContent(
|
||||
preferences: UserPreferences,
|
||||
navigationManager: NavigationManager,
|
||||
movie: BaseItem,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val dto = movie.data
|
||||
val backdropImageUrl = movie.backdropImageUrl
|
||||
val resumePosition = dto.userData?.playbackPositionTicks?.ticks ?: Duration.ZERO
|
||||
|
||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
bringIntoViewRequester.bringIntoView()
|
||||
focusRequester.tryRequestFocus()
|
||||
}
|
||||
Box(modifier = modifier) {
|
||||
if (backdropImageUrl.isNotNullOrBlank()) {
|
||||
val gradientColor = MaterialTheme.colorScheme.background
|
||||
AsyncImage(
|
||||
model = backdropImageUrl,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
alignment = Alignment.TopEnd,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxHeight(.75f)
|
||||
.alpha(.5f)
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
drawRect(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(Color.Transparent, gradientColor),
|
||||
startY = size.height * .5f,
|
||||
),
|
||||
)
|
||||
drawRect(
|
||||
Brush.horizontalGradient(
|
||||
colors = listOf(Color.Transparent, gradientColor),
|
||||
endX = 0f,
|
||||
startX = size.width * .75f,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(32.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
item {
|
||||
MovieDetailsHeader(
|
||||
movie = movie,
|
||||
bringIntoViewRequester = bringIntoViewRequester,
|
||||
overviewOnClick = {},
|
||||
Modifier
|
||||
.fillMaxWidth(.7f),
|
||||
)
|
||||
}
|
||||
item {
|
||||
ExpandablePlayButtons(
|
||||
resumePosition = resumePosition,
|
||||
watched = dto.userData?.played ?: false,
|
||||
playOnClick = {
|
||||
navigationManager.navigateTo(
|
||||
Destination.Playback(
|
||||
movie.id,
|
||||
it.inWholeMilliseconds,
|
||||
movie,
|
||||
),
|
||||
)
|
||||
},
|
||||
moreOnClick = {
|
||||
// TODO
|
||||
},
|
||||
watchOnClick = {
|
||||
// TODO
|
||||
},
|
||||
buttonOnFocusChanged = {
|
||||
scope.launch(ExceptionHandler()) {
|
||||
bringIntoViewRequester.bringIntoView()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
package com.github.damontecres.dolphin.ui.detail.movie
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shadow
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.dolphin.R
|
||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||
import com.github.damontecres.dolphin.ui.components.DotSeparatedRow
|
||||
import com.github.damontecres.dolphin.ui.components.StarRating
|
||||
import com.github.damontecres.dolphin.ui.components.StarRatingPrecision
|
||||
import com.github.damontecres.dolphin.ui.components.TitleValueText
|
||||
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.dolphin.ui.letNotEmpty
|
||||
import com.github.damontecres.dolphin.ui.playOnClickSound
|
||||
import com.github.damontecres.dolphin.ui.playSoundOnFocus
|
||||
import com.github.damontecres.dolphin.ui.roundMinutes
|
||||
import com.github.damontecres.dolphin.ui.timeRemaining
|
||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
|
||||
@Composable
|
||||
fun MovieDetailsHeader(
|
||||
movie: BaseItem,
|
||||
bringIntoViewRequester: BringIntoViewRequester,
|
||||
overviewOnClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val dto = movie.data
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
// Title
|
||||
Text(
|
||||
text = movie.name ?: "",
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style =
|
||||
MaterialTheme.typography.displayMedium.copy(
|
||||
shadow =
|
||||
Shadow(
|
||||
color = Color.DarkGray,
|
||||
offset = Offset(5f, 2f),
|
||||
blurRadius = 2f,
|
||||
),
|
||||
),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.alpha(0.75f),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
val details =
|
||||
buildList {
|
||||
dto.productionYear?.let { add(it.toString()) }
|
||||
val duration = dto.runTimeTicks?.ticks
|
||||
duration
|
||||
?.roundMinutes
|
||||
?.toString()
|
||||
?.let {
|
||||
add(it)
|
||||
}
|
||||
dto.timeRemaining?.roundMinutes?.let { add("$it left") }
|
||||
}
|
||||
DotSeparatedRow(
|
||||
texts = details,
|
||||
textStyle = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
}
|
||||
// TODO ratings?
|
||||
dto.communityRating?.let {
|
||||
if (it > 0f) {
|
||||
StarRating(
|
||||
rating100 = (it * 10).toInt(),
|
||||
onRatingChange = {},
|
||||
enabled = false,
|
||||
precision = StarRatingPrecision.HALF,
|
||||
playSoundOnFocus = true,
|
||||
modifier = Modifier.height(40.dp),
|
||||
)
|
||||
} else {
|
||||
Spacer(Modifier.height(40.dp))
|
||||
}
|
||||
}
|
||||
|
||||
// Description
|
||||
dto.overview?.let { overview ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isFocused = interactionSource.collectIsFocusedAsState().value
|
||||
val bgColor =
|
||||
if (isFocused) {
|
||||
MaterialTheme.colorScheme.onPrimary.copy(alpha = .4f)
|
||||
} else {
|
||||
Color.Unspecified
|
||||
}
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.background(bgColor, shape = RoundedCornerShape(8.dp))
|
||||
.playSoundOnFocus(true)
|
||||
.clickable(
|
||||
enabled = true,
|
||||
interactionSource = interactionSource,
|
||||
indication = LocalIndication.current,
|
||||
) {
|
||||
playOnClickSound(context)
|
||||
overviewOnClick.invoke()
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = overview,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(8.dp)
|
||||
.height(60.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
// Key-Values
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = 8.dp, start = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
dto.mediaStreams?.firstOrNull { it.type == MediaStreamType.VIDEO }?.displayTitle?.let {
|
||||
TitleValueText(
|
||||
stringResource(R.string.video),
|
||||
it,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
dto.mediaStreams
|
||||
?.firstOrNull { it.type == MediaStreamType.AUDIO }
|
||||
?.displayTitle
|
||||
?.let {
|
||||
// TODO probably a cleaner way to do this
|
||||
// Removes part of "5.1 Surround - English - AAC - Default"
|
||||
it
|
||||
.replace(" - Default", "")
|
||||
.ifBlank { null }
|
||||
?.let {
|
||||
TitleValueText(
|
||||
stringResource(R.string.audio),
|
||||
it,
|
||||
)
|
||||
}
|
||||
}
|
||||
dto.mediaStreams
|
||||
?.filter { it.type == MediaStreamType.SUBTITLE && it.language.isNotNullOrBlank() }
|
||||
?.mapNotNull { it.language }
|
||||
?.joinToString(", ")
|
||||
?.let {
|
||||
if (it.isNotNullOrBlank()) {
|
||||
TitleValueText(
|
||||
"Subtitles",
|
||||
it,
|
||||
modifier = Modifier.widthIn(max = 64.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
// TODO director
|
||||
// TODO writers
|
||||
dto.studios?.letNotEmpty {
|
||||
TitleValueText(
|
||||
stringResource(R.string.studios),
|
||||
it.joinToString(", ") { s -> s.name ?: "" },
|
||||
modifier = Modifier.widthIn(max = 80.dp),
|
||||
)
|
||||
}
|
||||
dto.genres?.letNotEmpty {
|
||||
TitleValueText(
|
||||
stringResource(R.string.studios),
|
||||
it.joinToString(", "),
|
||||
modifier = Modifier.widthIn(max = 80.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +1,20 @@
|
|||
package com.github.damontecres.dolphin.ui.detail.series
|
||||
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.damontecres.dolphin.R
|
||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||
import com.github.damontecres.dolphin.ui.components.ExpandableFaButton
|
||||
import com.github.damontecres.dolphin.ui.components.ExpandablePlayButton
|
||||
import com.github.damontecres.dolphin.ui.components.ExpandablePlayButtons
|
||||
import com.github.damontecres.dolphin.ui.components.TitleValueText
|
||||
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import kotlin.time.Duration
|
||||
|
|
@ -45,102 +35,41 @@ fun FocusedEpisodeFooter(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier,
|
||||
) {
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = PaddingValues(8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.focusGroup()
|
||||
.focusProperties {
|
||||
onEnter = {
|
||||
firstFocus.tryRequestFocus()
|
||||
}
|
||||
},
|
||||
) {
|
||||
if (resumePosition > Duration.ZERO) {
|
||||
item {
|
||||
// LaunchedEffect(Unit) { firstFocus.tryRequestFocus() }
|
||||
ExpandablePlayButton(
|
||||
R.string.resume,
|
||||
resumePosition,
|
||||
Icons.Default.PlayArrow,
|
||||
playOnClick,
|
||||
Modifier.focusRequester(firstFocus),
|
||||
// .onFocusChanged(buttonOnFocusChanged),
|
||||
)
|
||||
}
|
||||
item {
|
||||
ExpandablePlayButton(
|
||||
R.string.restart,
|
||||
Duration.ZERO,
|
||||
Icons.Default.Refresh,
|
||||
playOnClick,
|
||||
Modifier,
|
||||
// .onFocusChanged(buttonOnFocusChanged),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
ExpandablePlayButton(
|
||||
R.string.play,
|
||||
Duration.ZERO,
|
||||
Icons.Default.PlayArrow,
|
||||
playOnClick,
|
||||
Modifier.focusRequester(firstFocus),
|
||||
// .onFocusChanged(buttonOnFocusChanged)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val played = dto.userData?.played ?: false
|
||||
// Played button
|
||||
item {
|
||||
ExpandableFaButton(
|
||||
title = if (played) R.string.mark_unwatched else R.string.mark_watched,
|
||||
iconStringRes = if (played) R.string.fa_eye else R.string.fa_eye_slash,
|
||||
onClick = watchOnClick,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
|
||||
// More button
|
||||
item {
|
||||
ExpandablePlayButton(
|
||||
R.string.more,
|
||||
Duration.ZERO,
|
||||
Icons.Default.MoreVert,
|
||||
{ moreOnClick.invoke() },
|
||||
Modifier,
|
||||
// .onFocusChanged(buttonOnFocusChanged)
|
||||
)
|
||||
}
|
||||
}
|
||||
ExpandablePlayButtons(
|
||||
resumePosition = resumePosition,
|
||||
watched = dto.userData?.played ?: false,
|
||||
playOnClick = playOnClick,
|
||||
moreOnClick = moreOnClick,
|
||||
watchOnClick = watchOnClick,
|
||||
buttonOnFocusChanged = {},
|
||||
modifier = Modifier,
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
dto.mediaStreams
|
||||
?.firstOrNull { it.type == MediaStreamType.VIDEO }
|
||||
?.let { stream ->
|
||||
stream.displayTitle?.let {
|
||||
TitleValueText(
|
||||
"Video",
|
||||
it,
|
||||
)
|
||||
}
|
||||
?.displayTitle
|
||||
?.let {
|
||||
TitleValueText(
|
||||
stringResource(R.string.video),
|
||||
it,
|
||||
)
|
||||
}
|
||||
|
||||
dto.mediaStreams
|
||||
?.firstOrNull { it.type == MediaStreamType.AUDIO }
|
||||
?.let { stream ->
|
||||
?.displayTitle
|
||||
?.let {
|
||||
// TODO probably a cleaner way to do this
|
||||
// Removes part of "5.1 Surround - English - AAC - Default"
|
||||
stream.displayTitle
|
||||
?.replace(" - Default", "")
|
||||
?.ifBlank { null }
|
||||
it
|
||||
.replace(" - Default", "")
|
||||
.ifBlank { null }
|
||||
?.let {
|
||||
TitleValueText(
|
||||
"Audio",
|
||||
stringResource(R.string.audio),
|
||||
it,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import androidx.tv.material3.Text
|
|||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||
import com.github.damontecres.dolphin.ui.detail.CollectionFolderDetails
|
||||
import com.github.damontecres.dolphin.ui.detail.EpisodeDetails
|
||||
import com.github.damontecres.dolphin.ui.detail.MovieDetails
|
||||
import com.github.damontecres.dolphin.ui.detail.SeasonDetails
|
||||
import com.github.damontecres.dolphin.ui.detail.VideoDetails
|
||||
import com.github.damontecres.dolphin.ui.detail.movie.MovieDetails
|
||||
import com.github.damontecres.dolphin.ui.detail.series.SeasonEpisode
|
||||
import com.github.damontecres.dolphin.ui.detail.series.SeriesOverview
|
||||
import com.github.damontecres.dolphin.ui.main.MainPage
|
||||
|
|
|
|||
|
|
@ -38,4 +38,7 @@
|
|||
<string name="seek_bar_steps">Seek bar steps</string>
|
||||
<string name="max_homepage_items">Max items on home page rows</string>
|
||||
<string name="rewatch_next_up">Enable rewatching in next up</string>
|
||||
<string name="studios">Studios</string>
|
||||
<string name="video">Video</string>
|
||||
<string name="audio">Audio</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue