Move stream labels to top of description (#323)

Follow up to #279
Closes #162

Move the stream labels to the top under the dot-separated details. Also
move the rating up to the dot-separated details line and add it to home
page.
This commit is contained in:
damontecres 2025-11-25 15:51:30 -05:00 committed by GitHub
parent 5b96be6662
commit 0bfe3d2653
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 114 additions and 112 deletions

View file

@ -1,5 +1,6 @@
package com.github.damontecres.wholphin.ui.components
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -14,7 +15,7 @@ import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
@Composable
fun DetailsBackdropImage(
fun BoxScope.DetailsBackdropImage(
backdropImageUrl: String?,
modifier: Modifier = Modifier,
) {
@ -23,10 +24,11 @@ fun DetailsBackdropImage(
AsyncImage(
model = backdropImageUrl,
contentDescription = null,
contentScale = ContentScale.Crop,
contentScale = ContentScale.Fit,
alignment = Alignment.TopEnd,
modifier =
modifier
.align(Alignment.TopEnd)
.fillMaxHeight(.85f)
.alpha(.75f)
.drawWithContent {

View file

@ -23,20 +23,26 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.tv.material3.LocalTextStyle
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
@Composable
fun DotSeparatedRow(
texts: List<String>,
rating: Float? = null,
modifier: Modifier = Modifier,
textStyle: TextStyle = MaterialTheme.typography.titleSmall,
) {
CompositionLocalProvider(LocalTextStyle provides textStyle) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
@ -47,7 +53,7 @@ fun DotSeparatedRow(
style = textStyle,
color = MaterialTheme.colorScheme.onSurface,
)
if (index != texts.lastIndex) {
if (rating != null || index != texts.lastIndex) {
Box(
modifier =
Modifier
@ -58,5 +64,24 @@ fun DotSeparatedRow(
)
}
}
rating?.let {
SimpleStarRating(
communityRating = it,
modifier = Modifier,
)
}
}
}
}
@PreviewTvSpec
@Composable
private fun DotSeparatedRowPreview() {
WholphinTheme {
DotSeparatedRow(
texts = listOf("2025", "1h 48m", "PG-13", "1h 30m left"),
rating = 7.5f,
modifier = Modifier,
)
}
}

View file

@ -17,6 +17,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
@ -36,7 +37,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.Icon
import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.MaterialTheme
@ -77,17 +77,17 @@ val ratingBarHeight: Dp = 32.dp
fun SimpleStarRating(
communityRating: Float?,
modifier: Modifier = Modifier,
) = SimpleStarRating(
) = CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
SimpleStarRating(
text = communityRating?.let { String.format(Locale.getDefault(), "%.1f", it) },
modifier = modifier,
textColor = MaterialTheme.colorScheme.onSurface,
)
}
@Composable
fun SimpleStarRating(
text: String?,
modifier: Modifier = Modifier,
textColor: Color = LocalContentColor.current,
starColor: Color = FilledStarColor,
) {
Row(
@ -98,8 +98,6 @@ fun SimpleStarRating(
if (text.isNotNullOrBlank()) {
Text(
text = text,
fontSize = 14.sp,
color = textColor,
modifier = Modifier,
)
Icon(

View file

@ -197,7 +197,7 @@ fun StreamLabel(
modifier =
modifier
.background(
MaterialTheme.colorScheme.secondaryContainer,
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = .5f),
// MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(4.dp),
).padding(vertical = 4.dp, horizontal = 6.dp),
@ -245,8 +245,8 @@ private fun StreamLabelPreview() {
.padding(8.dp),
) {
StreamLabel("1080p")
StreamLabel("H264")
StreamLabel("HDR")
StreamLabel("H264")
StreamLabel("AC3 5.1", icon = R.string.fa_volume_high, count = 2)
}
}

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
@ -61,7 +62,6 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.Optional
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
import com.github.damontecres.wholphin.ui.components.chooseStream
import com.github.damontecres.wholphin.ui.components.chooseVersionParams
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
@ -391,7 +391,6 @@ fun MovieDetailsContent(
Modifier
.fillMaxWidth()
.bringIntoViewRequester(bringIntoViewRequester),
// .padding(bottom = 16.dp),
) {
MovieDetailsHeader(
preferences = preferences,
@ -399,7 +398,10 @@ fun MovieDetailsContent(
chosenStreams = chosenStreams,
bringIntoViewRequester = bringIntoViewRequester,
overviewOnClick = overviewOnClick,
modifier = Modifier.fillMaxWidth(),
modifier =
Modifier
.fillMaxWidth()
.padding(top = 32.dp, bottom = 16.dp),
)
ExpandablePlayButtons(
resumePosition = resumePosition,
@ -420,18 +422,13 @@ fun MovieDetailsContent(
}
}
},
modifier = Modifier.focusRequester(focusRequesters[HEADER_ROW]),
modifier =
Modifier
.padding(bottom = 16.dp)
.focusRequester(focusRequesters[HEADER_ROW]),
)
}
}
item {
VideoStreamDetails(
preferences = preferences,
dto = dto,
itemPlayback = chosenStreams?.itemPlayback,
modifier = Modifier,
)
}
if (people.isNotEmpty()) {
item {
PersonRow(

View file

@ -4,18 +4,17 @@ 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.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@ -27,7 +26,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
import com.github.damontecres.wholphin.ui.components.OverviewText
import com.github.damontecres.wholphin.ui.components.SimpleStarRating
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.roundMinutes
import com.github.damontecres.wholphin.ui.timeRemaining
@ -54,51 +53,49 @@ fun MovieDetailsHeader(
Text(
text = movie.name ?: "",
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.displayMedium,
style = MaterialTheme.typography.displaySmall,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(.75f),
)
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.fillMaxWidth(.60f),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
val padding = 8.dp
val details =
remember(dto) {
buildList {
dto.productionYear?.let { add(it.toString()) }
val duration = dto.runTimeTicks?.ticks
duration
?.roundMinutes
?.toString()
?.let {
add(it)
}
dto.officialRating?.let(::add)
?.let(::add)
dto.timeRemaining?.roundMinutes?.let { add("$it left") }
dto.officialRating?.let(::add)
}
}
DotSeparatedRow(
texts = details,
rating = dto.communityRating,
textStyle = MaterialTheme.typography.titleMedium,
modifier = Modifier,
modifier = Modifier.padding(bottom = padding),
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
SimpleStarRating(
dto.communityRating,
Modifier.height(20.dp),
VideoStreamDetails(
preferences = preferences,
dto = dto,
itemPlayback = chosenStreams?.itemPlayback,
modifier = Modifier.padding(bottom = padding),
)
}
dto.taglines?.firstOrNull()?.let { tagline ->
Text(
text = tagline,
style = MaterialTheme.typography.bodyLarge,
fontStyle = FontStyle.Italic,
modifier = Modifier,
)
}

View file

@ -11,7 +11,6 @@ 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.ExpandablePlayButtons
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
import org.jellyfin.sdk.model.extensions.ticks
import kotlin.time.Duration
@ -45,11 +44,5 @@ fun FocusedEpisodeFooter(
buttonOnFocusChanged = {},
modifier = Modifier,
)
VideoStreamDetails(
preferences = preferences,
dto = dto,
itemPlayback = chosenStreams?.itemPlayback,
modifier = Modifier,
)
}
}

View file

@ -2,10 +2,8 @@ package com.github.damontecres.wholphin.ui.detail.series
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
@ -17,7 +15,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
import com.github.damontecres.wholphin.ui.components.OverviewText
import com.github.damontecres.wholphin.ui.components.SimpleStarRating
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
import com.github.damontecres.wholphin.ui.formatDateTime
import com.github.damontecres.wholphin.ui.roundMinutes
import com.github.damontecres.wholphin.ui.seasonEpisode
@ -45,10 +43,8 @@ fun FocusedEpisodeHeader(
overflow = TextOverflow.Ellipsis,
modifier = Modifier,
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
val details =
remember(dto) {
buildList {
dto?.seasonEpisode?.let(::add)
dto?.premiereDate?.let { add(formatDateTime(it)) }
@ -56,24 +52,23 @@ fun FocusedEpisodeHeader(
duration
?.roundMinutes
?.toString()
?.let {
add(it)
}
dto?.officialRating?.let(::add)
?.let(::add)
dto?.timeRemaining?.roundMinutes?.let { add("$it left") }
dto?.officialRating?.let(::add)
}
}
DotSeparatedRow(
texts = details,
rating = dto?.communityRating,
textStyle = MaterialTheme.typography.titleSmall,
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
SimpleStarRating(
dto?.communityRating,
Modifier.height(20.dp),
if (dto != null) {
VideoStreamDetails(
preferences = preferences,
dto = dto,
itemPlayback = chosenStreams?.itemPlayback,
modifier = Modifier,
)
}
OverviewText(

View file

@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.relocation.BringIntoViewRequester
@ -30,6 +29,7 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
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.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@ -60,7 +60,6 @@ import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.Optional
import com.github.damontecres.wholphin.ui.components.OverviewText
import com.github.damontecres.wholphin.ui.components.SimpleStarRating
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
@ -584,23 +583,18 @@ fun SeriesDetailsHeader(
)
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,
modifier = Modifier,
)
}
SimpleStarRating(
dto.communityRating,
Modifier.height(20.dp),
)
dto.overview?.let { overview ->
OverviewText(
overview = overview,

View file

@ -502,6 +502,7 @@ fun HomePageHeader(
if (details.isNotEmpty()) {
DotSeparatedRow(
texts = details,
rating = item.data.communityRating,
textStyle = MaterialTheme.typography.bodyLarge,
modifier = Modifier,
)