mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
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:
parent
887b7b597e
commit
a197f6911b
9 changed files with 73 additions and 63 deletions
|
|
@ -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.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.seasonEpisode
|
import com.github.damontecres.wholphin.ui.seasonEpisode
|
||||||
import com.github.damontecres.wholphin.ui.seasonEpisodePadded
|
import com.github.damontecres.wholphin.ui.seasonEpisodePadded
|
||||||
|
import com.github.damontecres.wholphin.ui.seriesProductionYears
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
|
@ -28,7 +29,11 @@ data class BaseItem(
|
||||||
|
|
||||||
val subtitle
|
val subtitle
|
||||||
get() =
|
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 {
|
val subtitleLong: String? by lazy {
|
||||||
if (type == BaseItemKind.EPISODE) {
|
if (type == BaseItemKind.EPISODE) {
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,27 @@ val BaseItemDto.seasonEpisodePadded: String?
|
||||||
null
|
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")
|
private val abbrevSuffixes = listOf("", "K", "M", "B")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ val DefaultButtonPadding =
|
||||||
)
|
)
|
||||||
|
|
||||||
object Cards {
|
object Cards {
|
||||||
val height2x3 = 180.dp
|
val height2x3 = 172.dp
|
||||||
val playedPercentHeight = 6.dp
|
val playedPercentHeight = 6.dp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ fun PersonRow(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.focusRestorer(firstFocus),
|
.focusRestorer(firstFocus),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(people, key = { _, item -> item.id }) { index, item ->
|
itemsIndexed(people) { index, item ->
|
||||||
PersonCard(
|
PersonCard(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = { onClick.invoke(item) },
|
onClick = { onClick.invoke(item) },
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.ui.formatDateTime
|
import com.github.damontecres.wholphin.ui.formatDateTime
|
||||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||||
import com.github.damontecres.wholphin.ui.seasonEpisode
|
import com.github.damontecres.wholphin.ui.seasonEpisode
|
||||||
|
import com.github.damontecres.wholphin.ui.seriesProductionYears
|
||||||
import com.github.damontecres.wholphin.ui.timeRemaining
|
import com.github.damontecres.wholphin.ui.timeRemaining
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
|
|
@ -85,22 +86,7 @@ fun SeriesQuickDetails(
|
||||||
val details =
|
val details =
|
||||||
remember(dto) {
|
remember(dto) {
|
||||||
buildList {
|
buildList {
|
||||||
if (dto?.productionYear != null) {
|
dto?.seriesProductionYears?.let(::add)
|
||||||
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)
|
|
||||||
}
|
|
||||||
val duration = dto?.runTimeTicks?.ticks
|
val duration = dto?.runTimeTicks?.ticks
|
||||||
duration
|
duration
|
||||||
?.roundMinutes
|
?.roundMinutes
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.github.damontecres.wholphin.ui.components
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.focusGroup
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
|
@ -23,6 +24,7 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusDirection
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusProperties
|
import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
|
@ -58,9 +60,20 @@ fun TabRow(
|
||||||
modifier
|
modifier
|
||||||
.onFocusChanged {
|
.onFocusChanged {
|
||||||
rowHasFocus = it.hasFocus
|
rowHasFocus = it.hasFocus
|
||||||
}.focusProperties {
|
}.focusGroup()
|
||||||
|
.focusProperties {
|
||||||
onEnter = {
|
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()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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.DialogItem
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
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.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
|
||||||
|
|
@ -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.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
|
||||||
|
import com.github.damontecres.wholphin.ui.components.SeriesQuickDetails
|
||||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
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.letNotEmpty
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.rememberInt
|
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.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.MediaType
|
import org.jellyfin.sdk.model.api.MediaType
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
|
@ -572,17 +570,6 @@ fun SeriesDetailsHeader(
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val dto = series.data
|
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(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -598,13 +585,7 @@ fun SeriesDetailsHeader(
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
modifier = Modifier.fillMaxWidth(.60f),
|
modifier = Modifier.fillMaxWidth(.60f),
|
||||||
) {
|
) {
|
||||||
DotSeparatedRow(
|
SeriesQuickDetails(dto)
|
||||||
texts = details,
|
|
||||||
communityRating = dto.communityRating,
|
|
||||||
criticRating = dto.criticRating,
|
|
||||||
textStyle = MaterialTheme.typography.titleSmall,
|
|
||||||
modifier = Modifier,
|
|
||||||
)
|
|
||||||
dto.genres?.letNotEmpty {
|
dto.genres?.letNotEmpty {
|
||||||
GenreText(it)
|
GenreText(it)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,6 @@ fun SeriesOverviewContent(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.focusRequester(tabRowFocusRequester)
|
.focusRequester(tabRowFocusRequester)
|
||||||
.focusRestorer()
|
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
|
@ -246,9 +247,8 @@ fun HomePageContent(
|
||||||
item = focusedItem,
|
item = focusedItem,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth(.6f)
|
.padding(top = 48.dp, bottom = 32.dp, start = 32.dp)
|
||||||
.fillMaxHeight(.33f)
|
.fillMaxHeight(.33f),
|
||||||
.padding(16.dp),
|
|
||||||
)
|
)
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
|
|
@ -412,28 +412,32 @@ fun HomePageHeader(
|
||||||
item: BaseItem?,
|
item: BaseItem?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(
|
item?.let {
|
||||||
modifier = modifier,
|
val dto = item.data
|
||||||
) {
|
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
item?.let {
|
item.title?.let {
|
||||||
val dto = item.data
|
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 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 subtitle = if (isEpisode) dto.name else null
|
||||||
val overview = dto.overview
|
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 {
|
subtitle?.let {
|
||||||
Text(
|
Text(
|
||||||
text = subtitle,
|
text = subtitle,
|
||||||
|
|
@ -452,6 +456,7 @@ fun HomePageHeader(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(0.dp)
|
.padding(0.dp)
|
||||||
.height(48.dp + if (!isEpisode) 12.dp else 0.dp)
|
.height(48.dp + if (!isEpisode) 12.dp else 0.dp)
|
||||||
|
.width(400.dp)
|
||||||
if (overview.isNotNullOrBlank()) {
|
if (overview.isNotNullOrBlank()) {
|
||||||
Text(
|
Text(
|
||||||
text = overview,
|
text = overview,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue