Fixes to user interface (#374)

- Fixes a crash if the same person has multiple roles in an item
- Better tab focus behavior, especially on series pages
- Use consistent series production years on all pages
- Slightly decrease size and increase space on home page
This commit is contained in:
damontecres 2025-12-05 11:35:50 -05:00 committed by GitHub
parent 887b7b597e
commit a197f6911b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 73 additions and 63 deletions

View file

@ -5,6 +5,7 @@ import com.github.damontecres.wholphin.ui.formatDateTime
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.seasonEpisode
import com.github.damontecres.wholphin.ui.seasonEpisodePadded
import com.github.damontecres.wholphin.ui.seriesProductionYears
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import org.jellyfin.sdk.api.client.ApiClient
@ -28,7 +29,11 @@ data class BaseItem(
val subtitle
get() =
if (type == BaseItemKind.EPISODE) data.seasonEpisode + " - " + name else data.productionYear?.toString()
when (type) {
BaseItemKind.EPISODE -> data.seasonEpisode + " - " + name
BaseItemKind.SERIES -> data.seriesProductionYears
else -> data.productionYear?.toString()
}
val subtitleLong: String? by lazy {
if (type == BaseItemKind.EPISODE) {

View file

@ -54,6 +54,27 @@ val BaseItemDto.seasonEpisodePadded: String?
null
}
val BaseItemDto.seriesProductionYears: String?
get() =
if (productionYear != null) {
buildString {
append(productionYear.toString())
if (status == "Continuing") {
append(" - ")
append("Present")
} else if (status == "Ended") {
endDate?.let {
if (it.year != productionYear) {
append(" - ")
append(it.year)
}
}
}
}
} else {
null
}
private val abbrevSuffixes = listOf("", "K", "M", "B")
/**

View file

@ -75,7 +75,7 @@ val DefaultButtonPadding =
)
object Cards {
val height2x3 = 180.dp
val height2x3 = 172.dp
val playedPercentHeight = 6.dp
}

View file

@ -52,7 +52,7 @@ fun PersonRow(
.fillMaxWidth()
.focusRestorer(firstFocus),
) {
itemsIndexed(people, key = { _, item -> item.id }) { index, item ->
itemsIndexed(people) { index, item ->
PersonCard(
item = item,
onClick = { onClick.invoke(item) },

View file

@ -9,6 +9,7 @@ import androidx.tv.material3.Text
import com.github.damontecres.wholphin.ui.formatDateTime
import com.github.damontecres.wholphin.ui.roundMinutes
import com.github.damontecres.wholphin.ui.seasonEpisode
import com.github.damontecres.wholphin.ui.seriesProductionYears
import com.github.damontecres.wholphin.ui.timeRemaining
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.extensions.ticks
@ -85,22 +86,7 @@ fun SeriesQuickDetails(
val details =
remember(dto) {
buildList {
if (dto?.productionYear != null) {
val date =
buildString {
append(dto.productionYear.toString())
if (dto.status == "Continuing") {
append(" - ")
append("Present")
} else if (dto.status == "Ended") {
dto.endDate?.let {
append(" - ")
append(it.year)
}
}
}
add(date)
}
dto?.seriesProductionYears?.let(::add)
val duration = dto?.runTimeTicks?.ticks
duration
?.roundMinutes

View file

@ -3,6 +3,7 @@ package com.github.damontecres.wholphin.ui.components
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
@ -23,6 +24,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
@ -58,9 +60,20 @@ fun TabRow(
modifier
.onFocusChanged {
rowHasFocus = it.hasFocus
}.focusProperties {
}.focusGroup()
.focusProperties {
onEnter = {
focusRequesters.getOrNull(selectedTabIndex)?.tryRequestFocus()
// If entering from left or right, use last or first tab
// Otherwise use the selected tab
val focusRequester =
if (requestedFocusDirection == FocusDirection.Left) {
focusRequesters.lastOrNull()
} else if (requestedFocusDirection == FocusDirection.Right) {
focusRequesters.firstOrNull()
} else {
focusRequesters.getOrNull(selectedTabIndex)
}
(focusRequester ?: FocusRequester.Default).tryRequestFocus()
}
},
) {

View file

@ -55,7 +55,6 @@ import com.github.damontecres.wholphin.ui.components.DetailsBackdropImage
import com.github.damontecres.wholphin.ui.components.DialogItem
import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup
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
@ -63,6 +62,7 @@ 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
import com.github.damontecres.wholphin.ui.components.SeriesQuickDetails
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
@ -75,14 +75,12 @@ import com.github.damontecres.wholphin.ui.detail.movie.TrailerRow
import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.rememberInt
import com.github.damontecres.wholphin.ui.roundMinutes
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.extensions.ticks
import java.util.UUID
import kotlin.time.Duration
@ -572,17 +570,6 @@ fun SeriesDetailsHeader(
) {
val scope = rememberCoroutineScope()
val dto = series.data
val details =
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),
modifier = modifier,
@ -598,13 +585,7 @@ fun SeriesDetailsHeader(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.fillMaxWidth(.60f),
) {
DotSeparatedRow(
texts = details,
communityRating = dto.communityRating,
criticRating = dto.criticRating,
textStyle = MaterialTheme.typography.titleSmall,
modifier = Modifier,
)
SeriesQuickDetails(dto)
dto.genres?.letNotEmpty {
GenreText(it)
}

View file

@ -151,7 +151,6 @@ fun SeriesOverviewContent(
modifier =
Modifier
.focusRequester(tabRowFocusRequester)
.focusRestorer()
.padding(paddingValues)
.fillMaxWidth(),
)

View file

@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
@ -246,9 +247,8 @@ fun HomePageContent(
item = focusedItem,
modifier =
Modifier
.fillMaxWidth(.6f)
.fillMaxHeight(.33f)
.padding(16.dp),
.padding(top = 48.dp, bottom = 32.dp, start = 32.dp)
.fillMaxHeight(.33f),
)
LazyColumn(
state = listState,
@ -412,28 +412,32 @@ fun HomePageHeader(
item: BaseItem?,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier,
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxSize(),
) {
item?.let {
val dto = item.data
val isEpisode = item.type == BaseItemKind.EPISODE
val title = if (isEpisode) dto.seriesName ?: item.name else item.name
val subtitle = if (isEpisode) dto.name else null
val overview = dto.overview
title?.let {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier,
) {
item.title?.let {
Text(
text = title,
text = it,
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.SemiBold),
color = MaterialTheme.colorScheme.onBackground,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(.75f),
)
}
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier =
Modifier
.fillMaxWidth(.6f)
.fillMaxHeight(),
) {
val isEpisode = item.type == BaseItemKind.EPISODE
val subtitle = if (isEpisode) dto.name else null
val overview = dto.overview
subtitle?.let {
Text(
text = subtitle,
@ -452,6 +456,7 @@ fun HomePageHeader(
Modifier
.padding(0.dp)
.height(48.dp + if (!isEpisode) 12.dp else 0.dp)
.width(400.dp)
if (overview.isNotNullOrBlank()) {
Text(
text = overview,