diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt index fc307b34..ed121277 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt @@ -184,7 +184,7 @@ data class DiscoverItem( id = credit.id!!, type = SeerrItemType.fromString(credit.mediaType, SeerrItemType.PERSON), title = credit.name ?: credit.title, - subtitle = null, + subtitle = credit.character, overview = credit.overview, availability = SeerrAvailability.from(credit.mediaInfo?.status) @@ -199,7 +199,7 @@ data class DiscoverItem( id = credit.id!!, type = SeerrItemType.fromString(credit.mediaType, SeerrItemType.PERSON), title = credit.name ?: credit.title, - subtitle = null, + subtitle = credit.job, overview = credit.overview, availability = SeerrAvailability.from(credit.mediaInfo?.status) diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/Person.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/Person.kt index 3430640e..c12d37b5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/Person.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/Person.kt @@ -1,5 +1,6 @@ package com.github.damontecres.wholphin.data.model +import androidx.compose.runtime.Stable import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.imageApi import org.jellyfin.sdk.model.UUID @@ -7,6 +8,7 @@ import org.jellyfin.sdk.model.api.BaseItemPerson import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.PersonKind +@Stable data class Person( val id: UUID, val name: String?, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemCardImage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemCardImage.kt index 1a2bbacf..8964477a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemCardImage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemCardImage.kt @@ -113,6 +113,13 @@ fun ItemCardImage( modifier: Modifier = Modifier, useFallbackText: Boolean = true, contentScale: ContentScale = ContentScale.Fit, + fallback: @Composable BoxScope.() -> Unit = { + ItemCardImageFallback( + name = name, + useFallbackText = useFallbackText, + modifier = Modifier, + ) + }, ) { var imageError by remember(imageUrl) { mutableStateOf(false) } Box( @@ -134,11 +141,7 @@ fun ItemCardImage( .align(Alignment.TopCenter), ) } else { - ItemCardImageFallback( - name = name, - useFallbackText = useFallbackText, - modifier = Modifier, - ) + fallback.invoke(this) } if (showOverlay) { ItemCardImageOverlay( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonCard.kt index 4d0c0fe5..47fd4e94 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonCard.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonCard.kt @@ -1,37 +1,77 @@ package com.github.damontecres.wholphin.ui.cards import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.tv.material3.Border import androidx.tv.material3.Card import androidx.tv.material3.CardDefaults +import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text +import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.Person import com.github.damontecres.wholphin.ui.AspectRatios +import com.github.damontecres.wholphin.ui.FontAwesome +import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.enableMarquee +import com.github.damontecres.wholphin.ui.theme.WholphinTheme import kotlinx.coroutines.delay +import org.jellyfin.sdk.model.UUID +import org.jellyfin.sdk.model.api.PersonKind /** * A Card for a [Person] such as an actor or director */ @Composable fun PersonCard( - item: Person, + person: Person, + onClick: () -> Unit, + onLongClick: () -> Unit, + modifier: Modifier = Modifier, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) = PersonCard( + name = person.name, + role = person.role, + imageUrl = person.imageUrl, + favorite = person.favorite, + onClick = onClick, + onLongClick = onLongClick, + modifier = modifier, + interactionSource = interactionSource, +) + +@Composable +fun PersonCard( + name: String?, + role: String?, + imageUrl: String?, + favorite: Boolean, onClick: () -> Unit, onLongClick: () -> Unit, modifier: Modifier = Modifier, @@ -65,25 +105,62 @@ fun PersonCard( onClick = onClick, onLongClick = onLongClick, interactionSource = interactionSource, + shape = CardDefaults.shape(CircleShape), + border = + CardDefaults.border( + focusedBorder = + Border( + border = + BorderStroke( + width = 3.dp, + color = MaterialTheme.colorScheme.border, + ), + shape = CircleShape, + ), + ), colors = CardDefaults.colors( containerColor = Color.Transparent, ), ) { ItemCardImage( - imageUrl = item.imageUrl, - name = item.name, - showOverlay = true, - favorite = item.favorite, + imageUrl = imageUrl, + name = name, + showOverlay = false, + favorite = favorite, watched = false, unwatchedCount = -1, numberOfVersions = -1, watchedPercent = null, - useFallbackText = false, + useFallbackText = true, + contentScale = ContentScale.Crop, + fallback = { + Box( + modifier = + modifier + .background(MaterialTheme.colorScheme.surfaceVariant) + .fillMaxSize() + .align(Alignment.Center), + ) { + Text( + text = stringResource(R.string.fa_user), + fontFamily = FontAwesome, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 64.sp, + textAlign = TextAlign.Center, + modifier = + Modifier + .padding(8.dp) + .fillMaxWidth() + .align(Alignment.Center), + ) + } + }, modifier = Modifier .fillMaxWidth() - .aspectRatio(AspectRatios.TALL), // TODO, + .aspectRatio(AspectRatios.SQUARE) + .clip(CircleShape), ) } Column( @@ -94,7 +171,7 @@ fun PersonCard( .fillMaxWidth(), ) { Text( - text = item.name ?: "", + text = name ?: "", maxLines = 1, textAlign = TextAlign.Center, modifier = @@ -103,9 +180,9 @@ fun PersonCard( .padding(horizontal = 4.dp) .enableMarquee(focusedAfterDelay), ) - item.role?.let { + role?.let { Text( - text = item.role, + text = role, maxLines = 1, textAlign = TextAlign.Center, modifier = @@ -118,3 +195,24 @@ fun PersonCard( } } } + +@PreviewTvSpec +@Composable +private fun PersonCardPreview() { + WholphinTheme { + PersonCard( + person = + Person( + id = UUID.randomUUID(), + name = "John Smith", + role = "Actor", + type = PersonKind.ACTOR, + imageUrl = null, + favorite = false, + ), + onClick = {}, + onLongClick = {}, + modifier = Modifier.width(personRowCardWidth), + ) + } +} 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 b13ca932..f5b64ea0 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 @@ -21,6 +21,7 @@ import androidx.compose.ui.unit.dp import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.data.model.DiscoverItem import com.github.damontecres.wholphin.data.model.Person import com.github.damontecres.wholphin.ui.ifElse @@ -52,14 +53,14 @@ fun PersonRow( .fillMaxWidth() .focusRestorer(firstFocus), ) { - itemsIndexed(people) { index, item -> + itemsIndexed(people) { index, person -> PersonCard( - item = item, - onClick = { onClick.invoke(item) }, - onLongClick = { onLongClick?.invoke(index, item) }, + person = person, + onClick = { onClick.invoke(person) }, + onLongClick = { onLongClick?.invoke(index, person) }, modifier = Modifier - .width(120.dp) + .width(personRowCardWidth) .ifElse(index == 0, Modifier.focusRequester(firstFocus)) .animateItem(), ) @@ -67,3 +68,52 @@ fun PersonRow( } } } + +@Composable +fun DiscoverPersonRow( + people: List, + onClick: (DiscoverItem) -> Unit, + modifier: Modifier = Modifier, + @StringRes title: Int = R.string.people, + onLongClick: ((Int, DiscoverItem) -> Unit)? = null, +) { + val firstFocus = remember { FocusRequester() } + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier, + ) { + Text( + text = stringResource(title), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onBackground, + ) + LazyRow( + state = rememberLazyListState(), + horizontalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(8.dp), + modifier = + Modifier + .padding(start = 16.dp) + .fillMaxWidth() + .focusRestorer(firstFocus), + ) { + itemsIndexed(people) { index, person -> + PersonCard( + name = person.title, + role = person.subtitle, + imageUrl = person.posterUrl, + favorite = false, + onClick = { onClick.invoke(person) }, + onLongClick = { onLongClick?.invoke(index, person) }, + modifier = + Modifier + .width(personRowCardWidth) + .ifElse(index == 0, Modifier.focusRequester(firstFocus)) + .animateItem(), + ) + } + } + } +} + +val personRowCardWidth = 108.dp diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt index 40df8ee7..264af8cf 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt @@ -50,6 +50,7 @@ import com.github.damontecres.wholphin.services.SeerrUserConfig import com.github.damontecres.wholphin.services.TrailerService import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.cards.DiscoverItemCard +import com.github.damontecres.wholphin.ui.cards.DiscoverPersonRow import com.github.damontecres.wholphin.ui.cards.ItemRow import com.github.damontecres.wholphin.ui.cards.SeasonCard import com.github.damontecres.wholphin.ui.components.DialogItem @@ -60,12 +61,9 @@ import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo import com.github.damontecres.wholphin.ui.detail.MoreDialogActions -import com.github.damontecres.wholphin.ui.discover.DiscoverRow -import com.github.damontecres.wholphin.ui.discover.DiscoverRowData import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.rememberInt import com.github.damontecres.wholphin.ui.tryRequestFocus -import com.github.damontecres.wholphin.util.DataLoadingState import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import kotlinx.coroutines.launch @@ -325,22 +323,17 @@ fun DiscoverMovieDetailsContent( } if (people.isNotEmpty()) { item { - DiscoverRow( - row = - DiscoverRowData( - stringResource(R.string.people), - DataLoadingState.Success(people), - ), - onClickItem = { index: Int, item: DiscoverItem -> + DiscoverPersonRow( + people = people, + onClick = { position = PEOPLE_ROW - onClickPerson.invoke(item) + onClickPerson.invoke(it) }, - onLongClickItem = { index, person -> + onLongClick = { index, person -> position = PEOPLE_ROW onLongClickPerson.invoke(index, person) }, - onCardFocus = {}, - focusRequester = focusRequesters[PEOPLE_ROW], + modifier = Modifier.focusRequester(focusRequesters[PEOPLE_ROW]), ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt index 102ed668..1e6edcc3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt @@ -48,6 +48,7 @@ import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.services.SeerrUserConfig import com.github.damontecres.wholphin.services.TrailerService import com.github.damontecres.wholphin.ui.cards.DiscoverItemCard +import com.github.damontecres.wholphin.ui.cards.DiscoverPersonRow import com.github.damontecres.wholphin.ui.cards.ItemRow import com.github.damontecres.wholphin.ui.components.DialogItem import com.github.damontecres.wholphin.ui.components.DialogParams @@ -59,14 +60,11 @@ import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.OverviewText import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo -import com.github.damontecres.wholphin.ui.discover.DiscoverRow -import com.github.damontecres.wholphin.ui.discover.DiscoverRowData 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.DataLoadingState import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import kotlinx.coroutines.launch @@ -358,22 +356,17 @@ fun DiscoverSeriesDetailsContent( // } if (people.isNotEmpty()) { item { - DiscoverRow( - row = - DiscoverRowData( - stringResource(R.string.people), - DataLoadingState.Success(people), - ), - onClickItem = { index: Int, item: DiscoverItem -> + DiscoverPersonRow( + people = people, + onClick = { position = PEOPLE_ROW - onClickPerson.invoke(item) + onClickPerson.invoke(it) }, - onLongClickItem = { index, person -> + onLongClick = { index, person -> position = PEOPLE_ROW onLongClickPerson.invoke(index, person) }, - onCardFocus = {}, - focusRequester = focusRequesters[PEOPLE_ROW], + modifier = Modifier.focusRequester(focusRequesters[PEOPLE_ROW]), ) } }