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 org.jellyfin.sdk.model.extensions.ticks
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.contracts.ExperimentalContracts
|
import kotlin.contracts.ExperimentalContracts
|
||||||
|
import kotlin.contracts.InvocationKind
|
||||||
import kotlin.contracts.contract
|
import kotlin.contracts.contract
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.minutes
|
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.seekBack(amount: Duration) = seekTo((currentPosition - amount.inWholeMilliseconds).coerceAtLeast(0L))
|
||||||
|
|
||||||
fun Player.seekForward(amount: Duration) = seekTo((currentPosition + amount.inWholeMilliseconds).coerceAtMost(duration))
|
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.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.FocusState
|
import androidx.compose.ui.focus.FocusState
|
||||||
|
import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.focusRestorer
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
|
@ -26,6 +27,7 @@ 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 com.github.damontecres.dolphin.R
|
import com.github.damontecres.dolphin.R
|
||||||
|
import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
@Composable
|
@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.preferences.UserPreferences
|
||||||
import com.github.damontecres.dolphin.ui.components.ErrorMessage
|
import com.github.damontecres.dolphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.dolphin.ui.components.LoadingPage
|
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.Destination
|
||||||
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
||||||
import com.github.damontecres.dolphin.util.LoadingState
|
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
|
package com.github.damontecres.dolphin.ui.detail.series
|
||||||
|
|
||||||
import androidx.compose.foundation.focusGroup
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.widthIn
|
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.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusProperties
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.focus.focusRequester
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.github.damontecres.dolphin.R
|
import com.github.damontecres.dolphin.R
|
||||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
import com.github.damontecres.dolphin.ui.components.ExpandableFaButton
|
import com.github.damontecres.dolphin.ui.components.ExpandablePlayButtons
|
||||||
import com.github.damontecres.dolphin.ui.components.ExpandablePlayButton
|
|
||||||
import com.github.damontecres.dolphin.ui.components.TitleValueText
|
import com.github.damontecres.dolphin.ui.components.TitleValueText
|
||||||
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
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.api.MediaStreamType
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
@ -45,102 +35,41 @@ fun FocusedEpisodeFooter(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
LazyRow(
|
ExpandablePlayButtons(
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
resumePosition = resumePosition,
|
||||||
contentPadding = PaddingValues(8.dp),
|
watched = dto.userData?.played ?: false,
|
||||||
modifier =
|
playOnClick = playOnClick,
|
||||||
Modifier
|
moreOnClick = moreOnClick,
|
||||||
.focusGroup()
|
watchOnClick = watchOnClick,
|
||||||
.focusProperties {
|
buttonOnFocusChanged = {},
|
||||||
onEnter = {
|
modifier = Modifier,
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
dto.mediaStreams
|
dto.mediaStreams
|
||||||
?.firstOrNull { it.type == MediaStreamType.VIDEO }
|
?.firstOrNull { it.type == MediaStreamType.VIDEO }
|
||||||
?.let { stream ->
|
?.displayTitle
|
||||||
stream.displayTitle?.let {
|
?.let {
|
||||||
TitleValueText(
|
TitleValueText(
|
||||||
"Video",
|
stringResource(R.string.video),
|
||||||
it,
|
it,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.mediaStreams
|
dto.mediaStreams
|
||||||
?.firstOrNull { it.type == MediaStreamType.AUDIO }
|
?.firstOrNull { it.type == MediaStreamType.AUDIO }
|
||||||
?.let { stream ->
|
?.displayTitle
|
||||||
|
?.let {
|
||||||
// TODO probably a cleaner way to do this
|
// TODO probably a cleaner way to do this
|
||||||
// Removes part of "5.1 Surround - English - AAC - Default"
|
// Removes part of "5.1 Surround - English - AAC - Default"
|
||||||
stream.displayTitle
|
it
|
||||||
?.replace(" - Default", "")
|
.replace(" - Default", "")
|
||||||
?.ifBlank { null }
|
.ifBlank { null }
|
||||||
?.let {
|
?.let {
|
||||||
TitleValueText(
|
TitleValueText(
|
||||||
"Audio",
|
stringResource(R.string.audio),
|
||||||
it,
|
it,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.dolphin.ui.detail.CollectionFolderDetails
|
import com.github.damontecres.dolphin.ui.detail.CollectionFolderDetails
|
||||||
import com.github.damontecres.dolphin.ui.detail.EpisodeDetails
|
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.SeasonDetails
|
||||||
import com.github.damontecres.dolphin.ui.detail.VideoDetails
|
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.SeasonEpisode
|
||||||
import com.github.damontecres.dolphin.ui.detail.series.SeriesOverview
|
import com.github.damontecres.dolphin.ui.detail.series.SeriesOverview
|
||||||
import com.github.damontecres.dolphin.ui.main.MainPage
|
import com.github.damontecres.dolphin.ui.main.MainPage
|
||||||
|
|
|
||||||
|
|
@ -38,4 +38,7 @@
|
||||||
<string name="seek_bar_steps">Seek bar steps</string>
|
<string name="seek_bar_steps">Seek bar steps</string>
|
||||||
<string name="max_homepage_items">Max items on home page rows</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="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>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue