mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Updates series details, adds movie genres, & adds critic rating (#358)
## Details Updates the series details page to match style & layout of the new movie details page (from #279 & #327) Adds genre list to movie details, it was already on series details Adds critic rating using fresh or rotten tomato icons, if available ## Screenshots ### Movie  ### TV Show  ## Issues Closes #357 Closes #347 ## Attribution The fresh & rotten tomato images were sourced from: - https://commons.wikimedia.org/wiki/File:Rotten_Tomatoes.svg - https://commons.wikimedia.org/wiki/File:Rotten_Tomatoes_rotten.svg Credit: Emily Oberman from Pentagram, modified by Throast, Public domain, via Wikimedia Commons
This commit is contained in:
parent
fc330a3400
commit
8b33dd6d1f
14 changed files with 225 additions and 64 deletions
|
|
@ -39,7 +39,8 @@ import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||||
@Composable
|
@Composable
|
||||||
fun DotSeparatedRow(
|
fun DotSeparatedRow(
|
||||||
texts: List<String>,
|
texts: List<String>,
|
||||||
rating: Float? = null,
|
communityRating: Float? = null,
|
||||||
|
criticRating: Float? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
textStyle: TextStyle = MaterialTheme.typography.titleSmall,
|
textStyle: TextStyle = MaterialTheme.typography.titleSmall,
|
||||||
) {
|
) {
|
||||||
|
|
@ -54,27 +55,38 @@ fun DotSeparatedRow(
|
||||||
style = textStyle,
|
style = textStyle,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
if (rating != null || index != texts.lastIndex) {
|
if (communityRating != null || criticRating != null || index != texts.lastIndex) {
|
||||||
Box(
|
Dot()
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 1f))
|
|
||||||
.size(4.dp),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rating?.let {
|
communityRating?.let {
|
||||||
SimpleStarRating(
|
SimpleStarRating(
|
||||||
communityRating = it,
|
communityRating = it,
|
||||||
modifier = Modifier,
|
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
|
@PreviewTvSpec
|
||||||
@Composable
|
@Composable
|
||||||
private fun DotSeparatedRowPreview() {
|
private fun DotSeparatedRowPreview() {
|
||||||
|
|
@ -82,13 +94,22 @@ private fun DotSeparatedRowPreview() {
|
||||||
Column {
|
Column {
|
||||||
DotSeparatedRow(
|
DotSeparatedRow(
|
||||||
texts = listOf("2025", "1h 48m", "PG-13", "1h 30m left"),
|
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,
|
modifier = Modifier,
|
||||||
textStyle = MaterialTheme.typography.titleMedium,
|
textStyle = MaterialTheme.typography.titleMedium,
|
||||||
)
|
)
|
||||||
DotSeparatedRow(
|
DotSeparatedRow(
|
||||||
texts = listOf("2025", "1h 48m", "PG-13", "1h 30m left 7.5"),
|
texts = listOf("2025", "1h 48m", "PG-13", "1h 30m left 7.5"),
|
||||||
rating = 7.5f,
|
communityRating = 7.5f,
|
||||||
|
criticRating = .45f,
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
textStyle = MaterialTheme.typography.titleLarge,
|
textStyle = MaterialTheme.typography.titleLarge,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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<String>,
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,8 @@ fun MovieQuickDetails(
|
||||||
|
|
||||||
DotSeparatedRow(
|
DotSeparatedRow(
|
||||||
texts = details,
|
texts = details,
|
||||||
rating = dto?.communityRating,
|
communityRating = dto?.communityRating,
|
||||||
|
criticRating = dto?.criticRating,
|
||||||
textStyle = MaterialTheme.typography.titleSmall,
|
textStyle = MaterialTheme.typography.titleSmall,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.github.damontecres.wholphin.ui.components
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
import android.R.attr.textStyle
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.focusGroup
|
import androidx.compose.foundation.focusGroup
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
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.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
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.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
|
|
@ -44,6 +44,7 @@ import androidx.tv.material3.LocalContentColor
|
||||||
import androidx.tv.material3.LocalTextStyle
|
import androidx.tv.material3.LocalTextStyle
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.ui.AppColors
|
import com.github.damontecres.wholphin.ui.AppColors
|
||||||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
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
|
* Shows a rating out of 5 stars
|
||||||
*/
|
*/
|
||||||
|
|
@ -269,8 +306,12 @@ private fun SimpleStarRatingPreview() {
|
||||||
WholphinTheme {
|
WholphinTheme {
|
||||||
Column {
|
Column {
|
||||||
SimpleStarRating(7.5f, Modifier.height(32.dp))
|
SimpleStarRating(7.5f, Modifier.height(32.dp))
|
||||||
|
TomatoRating(75f)
|
||||||
SimpleStarRating(7.5f, Modifier.height(20.dp))
|
SimpleStarRating(7.5f, Modifier.height(20.dp))
|
||||||
SimpleStarRating("", Modifier.height(32.dp))
|
SimpleStarRating("", Modifier.height(32.dp))
|
||||||
|
TomatoRating(75f)
|
||||||
|
TomatoRating(40f)
|
||||||
|
TomatoRating(3f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,8 @@ fun EpisodeQuickDetails(
|
||||||
}
|
}
|
||||||
DotSeparatedRow(
|
DotSeparatedRow(
|
||||||
texts = details,
|
texts = details,
|
||||||
rating = dto?.communityRating,
|
communityRating = dto?.communityRating,
|
||||||
|
criticRating = dto?.criticRating,
|
||||||
textStyle = MaterialTheme.typography.titleSmall,
|
textStyle = MaterialTheme.typography.titleSmall,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import java.util.Locale
|
||||||
data class ItemDetailsDialogInfo(
|
data class ItemDetailsDialogInfo(
|
||||||
val title: String,
|
val title: String,
|
||||||
val overview: String?,
|
val overview: String?,
|
||||||
|
val genres: List<String>,
|
||||||
val files: List<MediaSourceInfo>,
|
val files: List<MediaSourceInfo>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -45,6 +46,14 @@ fun ItemDetailsDialog(
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (info.genres.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = info.genres.joinToString(", "),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (info.overview.isNotNullOrBlank()) {
|
if (info.overview.isNotNullOrBlank()) {
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,7 @@ fun PersonPage(
|
||||||
ItemDetailsDialogInfo(
|
ItemDetailsDialogInfo(
|
||||||
title = name,
|
title = name,
|
||||||
overview = person.data.overview,
|
overview = person.data.overview,
|
||||||
|
genres = listOf(),
|
||||||
files = listOf(),
|
files = listOf(),
|
||||||
),
|
),
|
||||||
showFilePath = false,
|
showFilePath = false,
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,7 @@ fun EpisodeDetails(
|
||||||
ItemDetailsDialogInfo(
|
ItemDetailsDialogInfo(
|
||||||
title = ep.name ?: context.getString(R.string.unknown),
|
title = ep.name ?: context.getString(R.string.unknown),
|
||||||
overview = ep.data.overview,
|
overview = ep.data.overview,
|
||||||
|
genres = ep.data.genres.orEmpty(),
|
||||||
files = ep.data.mediaSources.orEmpty(),
|
files = ep.data.mediaSources.orEmpty(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,7 @@ fun MovieDetails(
|
||||||
ItemDetailsDialogInfo(
|
ItemDetailsDialogInfo(
|
||||||
title = movie.name ?: context.getString(R.string.unknown),
|
title = movie.name ?: context.getString(R.string.unknown),
|
||||||
overview = movie.data.overview,
|
overview = movie.data.overview,
|
||||||
|
genres = movie.data.genres.orEmpty(),
|
||||||
files = movie.data.mediaSources.orEmpty(),
|
files = movie.data.mediaSources.orEmpty(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,15 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail.movie
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
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.ChosenStreams
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
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.MovieQuickDetails
|
||||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||||
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
|
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
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
|
import org.jellyfin.sdk.model.api.PersonKind
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -60,9 +62,13 @@ fun MovieDetailsHeader(
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
modifier = Modifier.fillMaxWidth(.60f),
|
modifier = Modifier.fillMaxWidth(.60f),
|
||||||
) {
|
) {
|
||||||
val padding = 8.dp
|
val padding = 4.dp
|
||||||
MovieQuickDetails(dto, Modifier.padding(bottom = padding))
|
MovieQuickDetails(dto, Modifier.padding(bottom = padding))
|
||||||
|
|
||||||
|
dto.genres?.letNotEmpty {
|
||||||
|
GenreText(it, Modifier.padding(bottom = padding))
|
||||||
|
}
|
||||||
|
|
||||||
VideoStreamDetails(
|
VideoStreamDetails(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
dto = dto,
|
dto = dto,
|
||||||
|
|
@ -80,17 +86,19 @@ fun MovieDetailsHeader(
|
||||||
|
|
||||||
// Description
|
// Description
|
||||||
dto.overview?.let { overview ->
|
dto.overview?.let { overview ->
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
val focused = interactionSource.collectIsFocusedAsState().value
|
|
||||||
LaunchedEffect(focused) {
|
|
||||||
if (focused) bringIntoViewRequester.bringIntoView()
|
|
||||||
}
|
|
||||||
OverviewText(
|
OverviewText(
|
||||||
overview = overview,
|
overview = overview,
|
||||||
maxLines = 3,
|
maxLines = 3,
|
||||||
onClick = overviewOnClick,
|
onClick = overviewOnClick,
|
||||||
textBoxHeight = Dp.Unspecified,
|
textBoxHeight = Dp.Unspecified,
|
||||||
interactionSource = interactionSource,
|
modifier =
|
||||||
|
Modifier.onFocusChanged {
|
||||||
|
if (it.isFocused) {
|
||||||
|
scope.launch(ExceptionHandler()) {
|
||||||
|
bringIntoViewRequester.bringIntoView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail.series
|
package com.github.damontecres.wholphin.ui.detail.series
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.compose.foundation.focusGroup
|
||||||
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
|
||||||
|
|
@ -25,6 +26,7 @@ import androidx.compose.runtime.setValue
|
||||||
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.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
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.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.ExpandableFaButton
|
import com.github.damontecres.wholphin.ui.components.ExpandableFaButton
|
||||||
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton
|
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.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.components.Optional
|
import com.github.damontecres.wholphin.ui.components.Optional
|
||||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||||
|
|
@ -174,6 +177,7 @@ fun SeriesDetails(
|
||||||
ItemDetailsDialogInfo(
|
ItemDetailsDialogInfo(
|
||||||
title = item.name ?: context.getString(R.string.unknown),
|
title = item.name ?: context.getString(R.string.unknown),
|
||||||
overview = item.data.overview,
|
overview = item.data.overview,
|
||||||
|
genres = item.data.genres.orEmpty(),
|
||||||
files = listOf(),
|
files = listOf(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -303,6 +307,7 @@ fun SeriesDetailsContent(
|
||||||
|
|
||||||
var position by rememberInt()
|
var position by rememberInt()
|
||||||
val focusRequesters = remember { List(SIMILAR_ROW + 1) { FocusRequester() } }
|
val focusRequesters = remember { List(SIMILAR_ROW + 1) { FocusRequester() } }
|
||||||
|
val playFocusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
||||||
}
|
}
|
||||||
|
|
@ -321,7 +326,7 @@ fun SeriesDetailsContent(
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = PaddingValues(bottom = 80.dp),
|
contentPadding = PaddingValues(bottom = 80.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
|
|
@ -330,9 +335,9 @@ fun SeriesDetailsContent(
|
||||||
overviewOnClick = overviewOnClick,
|
overviewOnClick = overviewOnClick,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth(.7f)
|
.fillMaxWidth()
|
||||||
.bringIntoViewRequester(bringIntoViewRequester)
|
.bringIntoViewRequester(bringIntoViewRequester)
|
||||||
.padding(bottom = 8.dp),
|
.padding(top = 32.dp, bottom = 16.dp),
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
|
@ -340,7 +345,9 @@ fun SeriesDetailsContent(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = 16.dp)
|
.padding(start = 16.dp)
|
||||||
.focusRequester(focusRequesters[HEADER_ROW])
|
.focusRequester(focusRequesters[HEADER_ROW])
|
||||||
.padding(bottom = 80.dp),
|
.focusRestorer(playFocusRequester)
|
||||||
|
.focusGroup()
|
||||||
|
.padding(bottom = 16.dp),
|
||||||
) {
|
) {
|
||||||
ExpandablePlayButton(
|
ExpandablePlayButton(
|
||||||
title = R.string.play,
|
title = R.string.play,
|
||||||
|
|
@ -351,13 +358,15 @@ fun SeriesDetailsContent(
|
||||||
playOnClick.invoke(false)
|
playOnClick.invoke(false)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.onFocusChanged {
|
Modifier
|
||||||
if (it.isFocused) {
|
.focusRequester(playFocusRequester)
|
||||||
scope.launch(ExceptionHandler()) {
|
.onFocusChanged {
|
||||||
bringIntoViewRequester.bringIntoView()
|
if (it.isFocused) {
|
||||||
|
scope.launch(ExceptionHandler()) {
|
||||||
|
bringIntoViewRequester.bringIntoView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
|
||||||
)
|
)
|
||||||
ExpandableFaButton(
|
ExpandableFaButton(
|
||||||
title = R.string.shuffle,
|
title = R.string.shuffle,
|
||||||
|
|
@ -406,7 +415,7 @@ fun SeriesDetailsContent(
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
ItemRow(
|
ItemRow(
|
||||||
title = stringResource(R.string.tv_seasons),
|
title = stringResource(R.string.tv_seasons) + " (${seasons.size})",
|
||||||
items = seasons,
|
items = seasons,
|
||||||
onClickItem = { index, item ->
|
onClickItem = { index, item ->
|
||||||
position = SEASONS_ROW
|
position = SEASONS_ROW
|
||||||
|
|
@ -564,13 +573,15 @@ fun SeriesDetailsHeader(
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val dto = series.data
|
val dto = series.data
|
||||||
val details =
|
val details =
|
||||||
buildList {
|
remember(series) {
|
||||||
dto.productionYear?.let { add(it.toString()) }
|
buildList {
|
||||||
dto.runTimeTicks
|
dto.productionYear?.let { add(it.toString()) }
|
||||||
?.ticks
|
dto.runTimeTicks
|
||||||
?.roundMinutes
|
?.ticks
|
||||||
?.let { add(it.toString()) }
|
?.roundMinutes
|
||||||
dto.officialRating?.let(::add)
|
?.let { add(it.toString()) }
|
||||||
|
dto.officialRating?.let(::add)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
|
@ -579,29 +590,32 @@ fun SeriesDetailsHeader(
|
||||||
Text(
|
Text(
|
||||||
text = series.name ?: stringResource(R.string.unknown),
|
text = series.name ?: stringResource(R.string.unknown),
|
||||||
style = MaterialTheme.typography.displaySmall,
|
style = MaterialTheme.typography.displaySmall,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.fillMaxWidth(.75f),
|
||||||
)
|
)
|
||||||
DotSeparatedRow(
|
Column(
|
||||||
texts = details,
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
rating = dto.communityRating,
|
modifier = Modifier.fillMaxWidth(.60f),
|
||||||
textStyle = MaterialTheme.typography.titleMedium,
|
) {
|
||||||
)
|
DotSeparatedRow(
|
||||||
dto.genres?.letNotEmpty {
|
texts = details,
|
||||||
Text(
|
communityRating = dto.communityRating,
|
||||||
text = it.joinToString(", "),
|
criticRating = dto.criticRating,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
textStyle = MaterialTheme.typography.titleSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
)
|
)
|
||||||
}
|
dto.genres?.letNotEmpty {
|
||||||
dto.overview?.let { overview ->
|
GenreText(it)
|
||||||
OverviewText(
|
}
|
||||||
overview = overview,
|
dto.overview?.let { overview ->
|
||||||
maxLines = 3,
|
OverviewText(
|
||||||
onClick = overviewOnClick,
|
overview = overview,
|
||||||
textBoxHeight = Dp.Unspecified,
|
maxLines = 3,
|
||||||
)
|
onClick = overviewOnClick,
|
||||||
|
textBoxHeight = Dp.Unspecified,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -332,6 +332,7 @@ fun SeriesOverview(
|
||||||
ItemDetailsDialogInfo(
|
ItemDetailsDialogInfo(
|
||||||
title = it.name ?: context.getString(R.string.unknown),
|
title = it.name ?: context.getString(R.string.unknown),
|
||||||
overview = it.data.overview,
|
overview = it.data.overview,
|
||||||
|
genres = it.data.genres.orEmpty(),
|
||||||
files = it.data.mediaSources.orEmpty(),
|
files = it.data.mediaSources.orEmpty(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
app/src/main/res/drawable/ic_rotten_tomatoes_fresh.xml
Normal file
15
app/src/main/res/drawable/ic_rotten_tomatoes_fresh.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="138.75dp"
|
||||||
|
android:height="141.25dp"
|
||||||
|
android:viewportWidth="138.75"
|
||||||
|
android:viewportHeight="141.25">
|
||||||
|
<path
|
||||||
|
android:pathData="m20.15,40.83c-28.15,27.62 -13.66,61.01 -5.73,71.93 35.25,41.95 92.79,25.34 111.89,-5.91 4.76,-8.2 22.55,-53.47 -23.98,-78.01z"
|
||||||
|
android:fillColor="#f93208"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m39.61,39.26 l4.78,-8.86 28.41,-5.04 11.12,9.21z"
|
||||||
|
android:fillColor="#f93208"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m39.44,8.57 l8.97,-5.28 6.76,15.48c3.79,-6.32 13.79,-16.32 24.94,-4.67 -4.73,1.26 -7.52,3.86 -7.74,8.48 15.15,-4.17 31.34,3.21 33.54,9.09 -10.95,-4.31 -27.69,10.38 -41.77,2.33 0.01,15.05 -12.62,16.64 -19.9,17.08 2.08,-5 5.59,-9.99 1.47,-14.99 -7.62,8.17 -13.87,10.67 -33.17,4.67 4.88,-1.68 14.84,-11.39 24.45,-11.43 -6.78,-2.47 -12.29,-2.09 -17.81,-1.48 2.92,-3.96 12.15,-15.2 28.63,-8.48z"
|
||||||
|
android:fillColor="#02902e"/>
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_rotten_tomatoes_rotten.xml
Normal file
9
app/src/main/res/drawable/ic_rotten_tomatoes_rotten.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="145dp"
|
||||||
|
android:height="140dp"
|
||||||
|
android:viewportWidth="145"
|
||||||
|
android:viewportHeight="140">
|
||||||
|
<path
|
||||||
|
android:pathData="M47.4,35.34c-13.61,-7.93 -12.32,-25.2 2.1,-31.88 26.12,-6.53 29.12,13.78 22.65,30.41 -6.54,24.11 18.09,23.66 19.92,10.07 3.61,-18.41 19.39,-26.69 31.67,-16.36 12.6,12.14 7.07,36.58 -17.83,34.19 -16.03,-1.54 -19.55,19.58 0.84,21.18 32.23,1.91 42.49,22.17 31.04,35.87 -15.99,15.15 -37.69,-4.44 -45.51,-19.5 -6.8,-9.31 -17.32,0.11 -13.42,6.5 12.98,19.47 2.92,31.23 -10.91,30.62 -13.37,-0.85 -20.96,-9.06 -13.21,-29.15 3.9,-12.48 -8.6,-15.39 -16.57,-5.45 -11.71,19.61 -28.86,13.68 -33.98,4.19 -3.24,-7.62 -2.92,-25.85 24.12,-23.7 16.69,4.14 11.78,-12.56 -0.63,-13.63 -9.24,-0.44 -30.5,-7.3 -22.86,-24.54 7.34,-11.06 24.96,-11.77 33.35,6.29 3.04,4.23 8.36,11.04 18.04,5.03 3.51,-5.2 1.21,-13.9 -8.81,-20.14z"
|
||||||
|
android:fillColor="#0fc755"/>
|
||||||
|
</vector>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue