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 78d7b85a..738bdbc5 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 @@ -2,6 +2,8 @@ package com.github.damontecres.wholphin.data.model +import com.github.damontecres.wholphin.api.seerr.model.CreditCast +import com.github.damontecres.wholphin.api.seerr.model.CreditCrew import com.github.damontecres.wholphin.api.seerr.model.MovieDetails import com.github.damontecres.wholphin.api.seerr.model.MovieMovieIdRatingsGet200Response import com.github.damontecres.wholphin.api.seerr.model.MovieResult @@ -164,6 +166,36 @@ data class DiscoverItem( backdropPath = search.backdropPath, jellyfinItemId = search.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), ) + + constructor(credit: CreditCast) : this( + id = credit.id!!, + type = SeerrItemType.TV, + title = credit.name, + subtitle = null, + overview = credit.overview, + availability = + SeerrAvailability.from(credit.mediaInfo?.status) + ?: SeerrAvailability.UNKNOWN, + releaseDate = credit.firstAirDate, + posterPath = credit.posterPath, + backdropPath = credit.backdropPath, + jellyfinItemId = credit.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), + ) + + constructor(credit: CreditCrew) : this( + id = credit.id!!, + type = SeerrItemType.TV, + title = credit.name, + subtitle = null, + overview = credit.overview, + availability = + SeerrAvailability.from(credit.mediaInfo?.status) + ?: SeerrAvailability.UNKNOWN, + releaseDate = credit.firstAirDate, + posterPath = credit.posterPath, + backdropPath = credit.backdropPath, + jellyfinItemId = credit.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), + ) } data class DiscoverRating( diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt index 78a37893..3107408f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt @@ -64,6 +64,24 @@ class SeerrService ?.map(::DiscoverItem) } + BaseItemKind.PERSON -> { + api.personApi + .personPersonIdCombinedCreditsGet(personId = it) + .let { credits -> + val cast = + credits.cast + ?.take(25) + ?.map(::DiscoverItem) + .orEmpty() + val crew = + credits.crew + ?.take(25) + ?.map(::DiscoverItem) + .orEmpty() + cast + crew + } + } + else -> { null } 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 319f7092..59143e47 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 @@ -72,6 +72,7 @@ val SlimItemFields = object Cards { val height2x3 = 172.dp + val heightEpisode = height2x3 * .75f val playedPercentHeight = 6.dp val serverUserCircle = height2x3 * .75f } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt index b715af8d..261a5a5d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -40,13 +41,17 @@ import androidx.tv.material3.surfaceColorAtElevation import coil3.compose.AsyncImage import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.DiscoverItem import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.services.FavoriteWatchManager import com.github.damontecres.wholphin.services.NavigationManager +import com.github.damontecres.wholphin.services.SeerrService +import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.LocalImageUrlService import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.SlimItemFields +import com.github.damontecres.wholphin.ui.cards.SeasonCard import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ExpandableFaButton import com.github.damontecres.wholphin.ui.components.LoadingPage @@ -54,7 +59,11 @@ import com.github.damontecres.wholphin.ui.components.LoadingRow 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.data.RowColumn +import com.github.damontecres.wholphin.ui.discover.DiscoverRow +import com.github.damontecres.wholphin.ui.discover.DiscoverRowData import com.github.damontecres.wholphin.ui.formatDate +import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.nav.Destination @@ -63,11 +72,14 @@ import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ApiRequestPager +import com.github.damontecres.wholphin.util.DataLoadingState import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.RowLoadingState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.ImageType @@ -86,10 +98,12 @@ class PersonViewModel api: ApiClient, val navigationManager: NavigationManager, private val favoriteWatchManager: FavoriteWatchManager, + private val seerrService: SeerrService, ) : LoadingItemViewModel(api) { val movies = MutableLiveData(RowLoadingState.Pending) val series = MutableLiveData(RowLoadingState.Pending) val episodes = MutableLiveData(RowLoadingState.Pending) + val discovered = MutableStateFlow>(listOf()) fun init(itemId: UUID) { viewModelScope.launchIO( @@ -115,6 +129,10 @@ class PersonViewModel } else { episodes.setValueOnMain(RowLoadingState.Success(listOf())) } + viewModelScope.launchIO { + val results = seerrService.similar(person) + discovered.update { results } + } } } } @@ -177,6 +195,7 @@ fun PersonPage( val movies by viewModel.movies.observeAsState(RowLoadingState.Pending) val series by viewModel.series.observeAsState(RowLoadingState.Pending) val episodes by viewModel.episodes.observeAsState(RowLoadingState.Pending) + val discovered by viewModel.discovered.collectAsState() val loading by viewModel.loading.observeAsState(LoadingState.Loading) when (val state = loading) { @@ -215,6 +234,10 @@ fun PersonPage( favoriteOnClick = { viewModel.setFavorite(!person.favorite) }, + discovered = discovered, + onClickDiscover = { index, item -> + viewModel.navigationManager.navigateTo(item.destination) + }, modifier = modifier, ) AnimatedVisibility(showOverviewDialog) { @@ -239,6 +262,7 @@ private const val HEADER_ROW = 0 private const val MOVIE_ROW = 1 private const val SERIES_ROW = 2 private const val EPISODE_ROW = 3 +private const val DISCOVER_ROW = EPISODE_ROW + 1 @Composable fun PersonPageContent( @@ -253,9 +277,11 @@ fun PersonPageContent( movies: RowLoadingState, series: RowLoadingState, episodes: RowLoadingState, + discovered: List, onClickItem: (Int, BaseItem) -> Unit, overviewOnClick: () -> Unit, favoriteOnClick: () -> Unit, + onClickDiscover: (Int, DiscoverItem) -> Unit, modifier: Modifier = Modifier, ) { val focusRequester = remember { FocusRequester() } @@ -336,8 +362,43 @@ fun PersonPageContent( showIfEmpty = false, horizontalPadding = 32.dp, modifier = Modifier.fillMaxWidth(), + cardContent = { index, item, mod, onClick, onLongClick -> + SeasonCard( + item = item, + onClick = { + position = RowColumn(EPISODE_ROW, index) + onClick.invoke() + }, + onLongClick = onLongClick, + imageHeight = Cards.heightEpisode, + modifier = + mod + .ifElse( + position.row == EPISODE_ROW && position.column == index, + Modifier.focusRequester(focusRequester), + ), + ) + }, ) } + if (discovered.isNotEmpty()) { + item { + DiscoverRow( + row = + DiscoverRowData( + stringResource(R.string.discover), + DataLoadingState.Success(discovered), + ), + onClickItem = { index: Int, item: DiscoverItem -> + position = RowColumn(DISCOVER_ROW, index) + onClickDiscover.invoke(index, item) + }, + onLongClickItem = { _, _ -> }, + onCardFocus = {}, + focusRequester = focusRequester, + ) + } + } } }