From a197f6911bcc119d1737926589795fa73f8de011 Mon Sep 17 00:00:00 2001 From: damontecres <154766448+damontecres@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:35:50 -0500 Subject: [PATCH] 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 --- .../wholphin/data/model/BaseItem.kt | 7 ++- .../damontecres/wholphin/ui/Formatting.kt | 21 +++++++++ .../damontecres/wholphin/ui/UiConstants.kt | 2 +- .../wholphin/ui/cards/PersonRow.kt | 2 +- .../ui/components/SeriesComponents.kt | 18 +------- .../wholphin/ui/components/TabRow.kt | 17 ++++++- .../ui/detail/series/SeriesDetails.kt | 23 +--------- .../ui/detail/series/SeriesOverviewContent.kt | 1 - .../damontecres/wholphin/ui/main/HomePage.kt | 45 ++++++++++--------- 9 files changed, 73 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt index fb2ed61d..4f2b03f8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt @@ -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) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt index 7641d399..f6d0e467 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt @@ -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") /** diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt index b01aad1d..39b2048b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt @@ -75,7 +75,7 @@ val DefaultButtonPadding = ) object Cards { - val height2x3 = 180.dp + val height2x3 = 172.dp val playedPercentHeight = 6.dp } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonRow.kt index 8c3a4c47..b13ca932 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonRow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonRow.kt @@ -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) }, 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 99dbb53b..36cbc904 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 @@ -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 diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabRow.kt index dafb5ed9..89323bdd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabRow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabRow.kt @@ -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() } }, ) { 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 0c5f379c..1ab8ebf4 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 @@ -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) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt index e5efbe3e..fe59c509 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt @@ -151,7 +151,6 @@ fun SeriesOverviewContent( modifier = Modifier .focusRequester(tabRowFocusRequester) - .focusRestorer() .padding(paddingValues) .fillMaxWidth(), ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt index dde9ed1d..a3c966eb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt @@ -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, - ) { + item?.let { + val dto = item.data Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier, ) { - item?.let { - val dto = item.data + item.title?.let { + Text( + 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 title = if (isEpisode) dto.seriesName ?: item.name else item.name val subtitle = if (isEpisode) dto.name else null val overview = dto.overview - title?.let { - Text( - text = title, - style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.SemiBold), - color = MaterialTheme.colorScheme.onBackground, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } 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,