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 8a283e4e..d60988a2 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 @@ -124,21 +124,7 @@ class SeerrService } BaseItemKind.PERSON -> { - api.personApi - .personPersonIdCombinedCreditsGet(personId = it) - .let { credits -> - val cast = - credits.cast - ?.take(25) - ?.map { createDiscoverItem(it) } - .orEmpty() - val crew = - credits.crew - ?.take(25) - ?.map { createDiscoverItem(it) } - .orEmpty() - cast + crew - } + personCredits(it) } else -> { @@ -150,6 +136,50 @@ class SeerrService null } + /** + * Get the combined movie & TV credits of a person as [DiscoverItem]s + * + * The two lists returned by the API are merged, de-duplicated by id since a person can be + * both cast and crew on the same title, and sorted by release date descending with undated + * entries last + */ + suspend fun personCredits(personId: Int): List = + api.personApi + .personPersonIdCombinedCreditsGet(personId = personId) + .let { credits -> + val cast = credits.cast?.map { createDiscoverItem(it) }.orEmpty() + val crew = credits.crew?.map { createDiscoverItem(it) }.orEmpty() + (cast + crew) + .distinctBy { it.id } + .sortedByDescending { it.releaseDate } + } + + /** + * Create a [DiscoverItem] for a person in the library so that the full grid of their + * credits can be opened from their page + * + * @return the item or null if the person has no TMDB id + */ + fun discoverPerson(item: BaseItem): DiscoverItem? = + item.data.providerIds + ?.get("Tmdb") + ?.toIntOrNull() + ?.let { + DiscoverItem( + id = it, + type = SeerrItemType.PERSON, + title = item.name, + subtitle = null, + overview = item.data.overview, + availability = SeerrAvailability.UNKNOWN, + releaseDate = null, + posterUrl = null, + backDropUrl = null, + logoUrl = null, + jellyfinItemId = null, + ) + } + suspend fun getTvSeries(item: BaseItem): TvDetails? = if (active.first()) { item.data.providerIds @@ -424,7 +454,7 @@ class SeerrService availability = SeerrAvailability.from(credit.mediaInfo?.status) ?: SeerrAvailability.UNKNOWN, - releaseDate = toLocalDate(credit.firstAirDate), + releaseDate = toLocalDate(credit.releaseDate ?: credit.firstAirDate), posterUrl = createImageUrl( ImageType.PRIMARY, @@ -451,7 +481,7 @@ class SeerrService availability = SeerrAvailability.from(credit.mediaInfo?.status) ?: SeerrAvailability.UNKNOWN, - releaseDate = toLocalDate(credit.firstAirDate), + releaseDate = toLocalDate(credit.releaseDate ?: credit.firstAirDate), posterUrl = createImageUrl( ImageType.PRIMARY, 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 8c9b05de..27212b5e 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 @@ -157,6 +157,7 @@ class PersonViewModel _state.update { it.copy( discovered = results, + discoverPerson = seerrService.discoverPerson(person), ) } } @@ -221,6 +222,7 @@ data class PersonState( val series: RowLoadingState = RowLoadingState.Pending, val episodes: RowLoadingState = RowLoadingState.Pending, val discovered: List = emptyList(), + val discoverPerson: DiscoverItem? = null, ) @Composable @@ -257,6 +259,12 @@ fun PersonPage( imageType = ImageType.PRIMARY, ) } + // A long filmography is hard to navigate as a row, so it is cut at the same + // limit as the home page rows and the rest is left to the full grid + val maxItems = preferences.appPreferences.homePagePreferences.maxItemsPerRow + val discovered = + remember(state.discovered, maxItems) { state.discovered.take(maxItems) } + PersonPageContent( preferences = preferences, name = name, @@ -276,10 +284,18 @@ fun PersonPage( favoriteOnClick = { viewModel.setFavorite(!person.favorite) }, - discovered = state.discovered, + discovered = discovered, onClickDiscover = { index, item -> viewModel.navigationManager.navigateTo(item.destination) }, + onClickViewMoreDiscover = { + state.discoverPerson?.let { + viewModel.navigationManager.navigateTo( + Destination.DiscoveredItem(it, maxItems), + ) + } + }, + enableViewMoreDiscover = state.discovered.size > discovered.size, modifier = modifier, ) AnimatedVisibility(showOverviewDialog) { @@ -323,6 +339,8 @@ fun PersonPageContent( overviewOnClick: () -> Unit, favoriteOnClick: () -> Unit, onClickDiscover: (Int, DiscoverItem) -> Unit, + onClickViewMoreDiscover: () -> Unit, + enableViewMoreDiscover: Boolean, modifier: Modifier = Modifier, ) { val focusRequester = remember { FocusRequester() } @@ -437,6 +455,11 @@ fun PersonPageContent( onLongClickItem = { _, _ -> }, onCardFocus = {}, focusRequester = focusRequester, + enableViewMore = enableViewMoreDiscover, + onClickViewMore = { + position = RowColumn(DISCOVER_ROW, discovered.size) + onClickViewMoreDiscover.invoke() + }, ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverPersonPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverPersonPage.kt index 5987b7eb..2ce4a3a9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverPersonPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverPersonPage.kt @@ -61,20 +61,7 @@ class DiscoverPersonViewModel viewModelScope.launchIO { backdropService.clearBackdrop() - val credits = - seerrService.api.personApi - .personPersonIdCombinedCreditsGet(personId = item.id) - .let { credits -> - val cast = - credits.cast - ?.map { seerrService.createDiscoverItem(it) } - .orEmpty() - val crew = - credits.crew - ?.map { seerrService.createDiscoverItem(it) } - .orEmpty() - cast + crew - } + val credits = seerrService.personCredits(item.id) this@DiscoverPersonViewModel.credits.update { DataLoadingState.Success(credits) } @@ -85,6 +72,7 @@ class DiscoverPersonViewModel @Composable fun DiscoverPersonPage( person: DiscoverItem, + startIndex: Int, modifier: Modifier = Modifier, viewModel: DiscoverPersonViewModel = hiltViewModel( @@ -137,6 +125,7 @@ fun DiscoverPersonPage( }, onClickPlay = { _, item -> }, + initialPosition = startIndex, letterPosition = { c: Char -> 0 }, gridFocusRequester = focusRequester, showJumpButtons = false, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt index 081b18d0..a65c97e8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt @@ -151,6 +151,8 @@ sealed class Destination( @Serializable data class DiscoveredItem( val item: DiscoverItem, + // Index to scroll to on the person page grid + val startIndex: Int = 0, ) : Destination(false) @Serializable diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt index f8497a8f..63789d6c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt @@ -405,6 +405,7 @@ fun DestinationContent( LaunchedEffect(Unit) { onClearBackdrop.invoke() } DiscoverPersonPage( person = destination.item, + startIndex = destination.startIndex, modifier = modifier, ) }