diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/DotSeparatedRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/DotSeparatedRow.kt index 8fa88aa6..4b04849c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/DotSeparatedRow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/DotSeparatedRow.kt @@ -39,7 +39,8 @@ import com.github.damontecres.wholphin.ui.theme.WholphinTheme @Composable fun DotSeparatedRow( texts: List, - rating: Float? = null, + communityRating: Float? = null, + criticRating: Float? = null, modifier: Modifier = Modifier, textStyle: TextStyle = MaterialTheme.typography.titleSmall, ) { @@ -54,27 +55,38 @@ fun DotSeparatedRow( style = textStyle, color = MaterialTheme.colorScheme.onSurface, ) - if (rating != null || index != texts.lastIndex) { - Box( - modifier = - Modifier - .padding(horizontal = 8.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.onSurface.copy(alpha = 1f)) - .size(4.dp), - ) + if (communityRating != null || criticRating != null || index != texts.lastIndex) { + Dot() } } - rating?.let { + communityRating?.let { SimpleStarRating( communityRating = it, modifier = Modifier, ) + if (criticRating != null) { + Dot() + } + } + criticRating?.let { + TomatoRating(it) } } } } +@Composable +fun Dot(modifier: Modifier = Modifier) { + Box( + modifier = + modifier + .padding(horizontal = 8.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.onSurface.copy(alpha = 1f)) + .size(4.dp), + ) +} + @PreviewTvSpec @Composable private fun DotSeparatedRowPreview() { @@ -82,13 +94,22 @@ private fun DotSeparatedRowPreview() { Column { DotSeparatedRow( texts = listOf("2025", "1h 48m", "PG-13", "1h 30m left"), - rating = 7.5f, + communityRating = null, + criticRating = .75f, + modifier = Modifier, + textStyle = MaterialTheme.typography.titleMedium, + ) + DotSeparatedRow( + texts = listOf("2025", "1h 48m", "PG-13", "1h 30m left"), + communityRating = 7.5f, + criticRating = .75f, modifier = Modifier, textStyle = MaterialTheme.typography.titleMedium, ) DotSeparatedRow( texts = listOf("2025", "1h 48m", "PG-13", "1h 30m left 7.5"), - rating = 7.5f, + communityRating = 7.5f, + criticRating = .45f, modifier = Modifier, textStyle = MaterialTheme.typography.titleLarge, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreText.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreText.kt new file mode 100644 index 00000000..3598fb93 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreText.kt @@ -0,0 +1,38 @@ +package com.github.damontecres.wholphin.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Text + +private const val MAX_TO_SHOW = 4 + +@Composable +fun GenreText( + genres: List, + modifier: Modifier = Modifier, + textStyle: TextStyle = MaterialTheme.typography.bodyMedium, + color: Color = MaterialTheme.colorScheme.onSurface, +) { + val text = + remember(genres) { + if (genres.size <= MAX_TO_SHOW) { + genres.joinToString(", ") + } else { + genres + .subList(0, MAX_TO_SHOW) + .joinToString(", ") + ", +${genres.size - MAX_TO_SHOW}" + } + } + Text( + text = text, + style = textStyle, + color = color, + overflow = TextOverflow.Ellipsis, + modifier = modifier, + ) +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/MovieComponents.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/MovieComponents.kt index 5247f801..30e0a902 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/MovieComponents.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/MovieComponents.kt @@ -30,7 +30,8 @@ fun MovieQuickDetails( DotSeparatedRow( texts = details, - rating = dto?.communityRating, + communityRating = dto?.communityRating, + criticRating = dto?.criticRating, textStyle = MaterialTheme.typography.titleSmall, modifier = modifier, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Rating.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Rating.kt index c849462c..b58a88cb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Rating.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Rating.kt @@ -1,6 +1,5 @@ package com.github.damontecres.wholphin.ui.components -import android.R.attr.textStyle import androidx.compose.foundation.background import androidx.compose.foundation.focusGroup import androidx.compose.foundation.layout.Arrangement @@ -37,6 +36,7 @@ import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.tv.material3.Icon @@ -44,6 +44,7 @@ import androidx.tv.material3.LocalContentColor import androidx.tv.material3.LocalTextStyle import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text +import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.isNotNullOrBlank @@ -114,6 +115,42 @@ fun SimpleStarRating( } } +@Composable +fun TomatoRating( + rating: Float?, + modifier: Modifier = Modifier, + threshold: Float = 60f, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = modifier, + ) { + if (rating != null) { + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) { + Text( + text = rating.toInt().toString() + "%", + modifier = Modifier, + ) + } + val height = with(LocalDensity.current) { LocalTextStyle.current.fontSize.toDp() * .8f } + Icon( + painter = + painterResource( + if (rating >= threshold) { + R.drawable.ic_rotten_tomatoes_fresh + } else { + R.drawable.ic_rotten_tomatoes_rotten + }, + ), + contentDescription = null, + modifier = Modifier.height(height), + tint = Color.Unspecified, + ) + } + } +} + /** * Shows a rating out of 5 stars */ @@ -269,8 +306,12 @@ private fun SimpleStarRatingPreview() { WholphinTheme { Column { SimpleStarRating(7.5f, Modifier.height(32.dp)) + TomatoRating(75f) SimpleStarRating(7.5f, Modifier.height(20.dp)) SimpleStarRating("", Modifier.height(32.dp)) + TomatoRating(75f) + TomatoRating(40f) + TomatoRating(3f) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/SeriesComponents.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/SeriesComponents.kt index 1f96967b..8fb63ef6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/SeriesComponents.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/SeriesComponents.kt @@ -70,7 +70,8 @@ fun EpisodeQuickDetails( } DotSeparatedRow( texts = details, - rating = dto?.communityRating, + communityRating = dto?.communityRating, + criticRating = dto?.criticRating, textStyle = MaterialTheme.typography.titleSmall, modifier = modifier, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt index d305bfe1..cf3e2896 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt @@ -28,6 +28,7 @@ import java.util.Locale data class ItemDetailsDialogInfo( val title: String, val overview: String?, + val genres: List, val files: List, ) @@ -45,6 +46,14 @@ fun ItemDetailsDialog( style = MaterialTheme.typography.titleLarge, ) } + if (info.genres.isNotEmpty()) { + item { + Text( + text = info.genres.joinToString(", "), + style = MaterialTheme.typography.titleSmall, + ) + } + } if (info.overview.isNotNullOrBlank()) { item { Text( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt index 5a33edcb..e315a396 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt @@ -212,6 +212,7 @@ fun PersonPage( ItemDetailsDialogInfo( title = name, overview = person.data.overview, + genres = listOf(), files = listOf(), ), showFilePath = false, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt index 7425449d..034f6855 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt @@ -136,6 +136,7 @@ fun EpisodeDetails( ItemDetailsDialogInfo( title = ep.name ?: context.getString(R.string.unknown), overview = ep.data.overview, + genres = ep.data.genres.orEmpty(), files = ep.data.mediaSources.orEmpty(), ) }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index 6ba7ae76..6e44b334 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -182,6 +182,7 @@ fun MovieDetails( ItemDetailsDialogInfo( title = movie.name ?: context.getString(R.string.unknown), overview = movie.data.overview, + genres = movie.data.genres.orEmpty(), files = movie.data.mediaSources.orEmpty(), ) }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt index a2d68ca9..71115a9a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt @@ -1,17 +1,15 @@ package com.github.damontecres.wholphin.ui.detail.movie -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle @@ -24,10 +22,14 @@ import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ChosenStreams import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.ui.components.GenreText import com.github.damontecres.wholphin.ui.components.MovieQuickDetails import com.github.damontecres.wholphin.ui.components.OverviewText import com.github.damontecres.wholphin.ui.components.VideoStreamDetails import com.github.damontecres.wholphin.ui.isNotNullOrBlank +import com.github.damontecres.wholphin.ui.letNotEmpty +import com.github.damontecres.wholphin.util.ExceptionHandler +import kotlinx.coroutines.launch import org.jellyfin.sdk.model.api.PersonKind @Composable @@ -60,9 +62,13 @@ fun MovieDetailsHeader( verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.fillMaxWidth(.60f), ) { - val padding = 8.dp + val padding = 4.dp MovieQuickDetails(dto, Modifier.padding(bottom = padding)) + dto.genres?.letNotEmpty { + GenreText(it, Modifier.padding(bottom = padding)) + } + VideoStreamDetails( preferences = preferences, dto = dto, @@ -80,17 +86,19 @@ fun MovieDetailsHeader( // Description dto.overview?.let { overview -> - val interactionSource = remember { MutableInteractionSource() } - val focused = interactionSource.collectIsFocusedAsState().value - LaunchedEffect(focused) { - if (focused) bringIntoViewRequester.bringIntoView() - } OverviewText( overview = overview, maxLines = 3, onClick = overviewOnClick, textBoxHeight = Dp.Unspecified, - interactionSource = interactionSource, + modifier = + Modifier.onFocusChanged { + if (it.isFocused) { + scope.launch(ExceptionHandler()) { + bringIntoViewRequester.bringIntoView() + } + } + }, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt index d1a6143a..7675d949 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt @@ -1,6 +1,7 @@ package com.github.damontecres.wholphin.ui.detail.series import android.content.Context +import androidx.compose.foundation.focusGroup import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -25,6 +26,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext @@ -57,6 +59,7 @@ import com.github.damontecres.wholphin.ui.components.DotSeparatedRow import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ExpandableFaButton import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton +import com.github.damontecres.wholphin.ui.components.GenreText import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.Optional import com.github.damontecres.wholphin.ui.components.OverviewText @@ -174,6 +177,7 @@ fun SeriesDetails( ItemDetailsDialogInfo( title = item.name ?: context.getString(R.string.unknown), overview = item.data.overview, + genres = item.data.genres.orEmpty(), files = listOf(), ) }, @@ -303,6 +307,7 @@ fun SeriesDetailsContent( var position by rememberInt() val focusRequesters = remember { List(SIMILAR_ROW + 1) { FocusRequester() } } + val playFocusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { focusRequesters.getOrNull(position)?.tryRequestFocus() } @@ -321,7 +326,7 @@ fun SeriesDetailsContent( ) { LazyColumn( contentPadding = PaddingValues(bottom = 80.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(0.dp), modifier = Modifier, ) { item { @@ -330,9 +335,9 @@ fun SeriesDetailsContent( overviewOnClick = overviewOnClick, modifier = Modifier - .fillMaxWidth(.7f) + .fillMaxWidth() .bringIntoViewRequester(bringIntoViewRequester) - .padding(bottom = 8.dp), + .padding(top = 32.dp, bottom = 16.dp), ) Row( horizontalArrangement = Arrangement.spacedBy(16.dp), @@ -340,7 +345,9 @@ fun SeriesDetailsContent( Modifier .padding(start = 16.dp) .focusRequester(focusRequesters[HEADER_ROW]) - .padding(bottom = 80.dp), + .focusRestorer(playFocusRequester) + .focusGroup() + .padding(bottom = 16.dp), ) { ExpandablePlayButton( title = R.string.play, @@ -351,13 +358,15 @@ fun SeriesDetailsContent( playOnClick.invoke(false) }, modifier = - Modifier.onFocusChanged { - if (it.isFocused) { - scope.launch(ExceptionHandler()) { - bringIntoViewRequester.bringIntoView() + Modifier + .focusRequester(playFocusRequester) + .onFocusChanged { + if (it.isFocused) { + scope.launch(ExceptionHandler()) { + bringIntoViewRequester.bringIntoView() + } } - } - }, + }, ) ExpandableFaButton( title = R.string.shuffle, @@ -406,7 +415,7 @@ fun SeriesDetailsContent( } item { ItemRow( - title = stringResource(R.string.tv_seasons), + title = stringResource(R.string.tv_seasons) + " (${seasons.size})", items = seasons, onClickItem = { index, item -> position = SEASONS_ROW @@ -564,13 +573,15 @@ fun SeriesDetailsHeader( val scope = rememberCoroutineScope() val dto = series.data val details = - buildList { - dto.productionYear?.let { add(it.toString()) } - dto.runTimeTicks - ?.ticks - ?.roundMinutes - ?.let { add(it.toString()) } - dto.officialRating?.let(::add) + remember(series) { + buildList { + dto.productionYear?.let { add(it.toString()) } + dto.runTimeTicks + ?.ticks + ?.roundMinutes + ?.let { add(it.toString()) } + dto.officialRating?.let(::add) + } } Column( verticalArrangement = Arrangement.spacedBy(8.dp), @@ -579,29 +590,32 @@ fun SeriesDetailsHeader( Text( text = series.name ?: stringResource(R.string.unknown), style = MaterialTheme.typography.displaySmall, - modifier = Modifier.fillMaxWidth(), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth(.75f), ) - DotSeparatedRow( - texts = details, - rating = dto.communityRating, - textStyle = MaterialTheme.typography.titleMedium, - ) - dto.genres?.letNotEmpty { - Text( - text = it.joinToString(", "), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, - overflow = TextOverflow.Ellipsis, + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.fillMaxWidth(.60f), + ) { + DotSeparatedRow( + texts = details, + communityRating = dto.communityRating, + criticRating = dto.criticRating, + textStyle = MaterialTheme.typography.titleSmall, modifier = Modifier, ) - } - dto.overview?.let { overview -> - OverviewText( - overview = overview, - maxLines = 3, - onClick = overviewOnClick, - textBoxHeight = Dp.Unspecified, - ) + dto.genres?.letNotEmpty { + GenreText(it) + } + dto.overview?.let { overview -> + OverviewText( + overview = overview, + maxLines = 3, + onClick = overviewOnClick, + textBoxHeight = Dp.Unspecified, + ) + } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt index 5fe6ee64..b1fb4ab9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt @@ -332,6 +332,7 @@ fun SeriesOverview( ItemDetailsDialogInfo( title = it.name ?: context.getString(R.string.unknown), overview = it.data.overview, + genres = it.data.genres.orEmpty(), files = it.data.mediaSources.orEmpty(), ) } diff --git a/app/src/main/res/drawable/ic_rotten_tomatoes_fresh.xml b/app/src/main/res/drawable/ic_rotten_tomatoes_fresh.xml new file mode 100644 index 00000000..5dbfb65e --- /dev/null +++ b/app/src/main/res/drawable/ic_rotten_tomatoes_fresh.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_rotten_tomatoes_rotten.xml b/app/src/main/res/drawable/ic_rotten_tomatoes_rotten.xml new file mode 100644 index 00000000..1e3dc568 --- /dev/null +++ b/app/src/main/res/drawable/ic_rotten_tomatoes_rotten.xml @@ -0,0 +1,9 @@ + + +