Discover on person page

This commit is contained in:
Damontecres 2026-01-06 18:02:40 -05:00
parent e7060cedfd
commit d454cf0e3d
No known key found for this signature in database
4 changed files with 112 additions and 0 deletions

View file

@ -2,6 +2,8 @@
package com.github.damontecres.wholphin.data.model 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.MovieDetails
import com.github.damontecres.wholphin.api.seerr.model.MovieMovieIdRatingsGet200Response import com.github.damontecres.wholphin.api.seerr.model.MovieMovieIdRatingsGet200Response
import com.github.damontecres.wholphin.api.seerr.model.MovieResult import com.github.damontecres.wholphin.api.seerr.model.MovieResult
@ -164,6 +166,36 @@ data class DiscoverItem(
backdropPath = search.backdropPath, backdropPath = search.backdropPath,
jellyfinItemId = search.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), 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( data class DiscoverRating(

View file

@ -64,6 +64,24 @@ class SeerrService
?.map(::DiscoverItem) ?.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 -> { else -> {
null null
} }

View file

@ -72,6 +72,7 @@ val SlimItemFields =
object Cards { object Cards {
val height2x3 = 172.dp val height2x3 = 172.dp
val heightEpisode = height2x3 * .75f
val playedPercentHeight = 6.dp val playedPercentHeight = 6.dp
val serverUserCircle = height2x3 * .75f val serverUserCircle = height2x3 * .75f
} }

View file

@ -16,6 +16,7 @@ import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -40,13 +41,17 @@ import androidx.tv.material3.surfaceColorAtElevation
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem 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.preferences.UserPreferences
import com.github.damontecres.wholphin.services.FavoriteWatchManager import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.NavigationManager 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.LocalImageUrlService
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.SlimItemFields 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.ErrorMessage
import com.github.damontecres.wholphin.ui.components.ExpandableFaButton import com.github.damontecres.wholphin.ui.components.ExpandableFaButton
import com.github.damontecres.wholphin.ui.components.LoadingPage 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.components.OverviewText
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
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.formatDate
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination 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.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ApiRequestPager 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.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.RowLoadingState import com.github.damontecres.wholphin.util.RowLoadingState
import dagger.hilt.android.lifecycle.HiltViewModel 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.api.client.ApiClient
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.ImageType
@ -86,10 +98,12 @@ class PersonViewModel
api: ApiClient, api: ApiClient,
val navigationManager: NavigationManager, val navigationManager: NavigationManager,
private val favoriteWatchManager: FavoriteWatchManager, private val favoriteWatchManager: FavoriteWatchManager,
private val seerrService: SeerrService,
) : LoadingItemViewModel(api) { ) : LoadingItemViewModel(api) {
val movies = MutableLiveData<RowLoadingState>(RowLoadingState.Pending) val movies = MutableLiveData<RowLoadingState>(RowLoadingState.Pending)
val series = MutableLiveData<RowLoadingState>(RowLoadingState.Pending) val series = MutableLiveData<RowLoadingState>(RowLoadingState.Pending)
val episodes = MutableLiveData<RowLoadingState>(RowLoadingState.Pending) val episodes = MutableLiveData<RowLoadingState>(RowLoadingState.Pending)
val discovered = MutableStateFlow<List<DiscoverItem>>(listOf())
fun init(itemId: UUID) { fun init(itemId: UUID) {
viewModelScope.launchIO( viewModelScope.launchIO(
@ -115,6 +129,10 @@ class PersonViewModel
} else { } else {
episodes.setValueOnMain(RowLoadingState.Success(listOf())) 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 movies by viewModel.movies.observeAsState(RowLoadingState.Pending)
val series by viewModel.series.observeAsState(RowLoadingState.Pending) val series by viewModel.series.observeAsState(RowLoadingState.Pending)
val episodes by viewModel.episodes.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) val loading by viewModel.loading.observeAsState(LoadingState.Loading)
when (val state = loading) { when (val state = loading) {
@ -215,6 +234,10 @@ fun PersonPage(
favoriteOnClick = { favoriteOnClick = {
viewModel.setFavorite(!person.favorite) viewModel.setFavorite(!person.favorite)
}, },
discovered = discovered,
onClickDiscover = { index, item ->
viewModel.navigationManager.navigateTo(item.destination)
},
modifier = modifier, modifier = modifier,
) )
AnimatedVisibility(showOverviewDialog) { AnimatedVisibility(showOverviewDialog) {
@ -239,6 +262,7 @@ private const val HEADER_ROW = 0
private const val MOVIE_ROW = 1 private const val MOVIE_ROW = 1
private const val SERIES_ROW = 2 private const val SERIES_ROW = 2
private const val EPISODE_ROW = 3 private const val EPISODE_ROW = 3
private const val DISCOVER_ROW = EPISODE_ROW + 1
@Composable @Composable
fun PersonPageContent( fun PersonPageContent(
@ -253,9 +277,11 @@ fun PersonPageContent(
movies: RowLoadingState, movies: RowLoadingState,
series: RowLoadingState, series: RowLoadingState,
episodes: RowLoadingState, episodes: RowLoadingState,
discovered: List<DiscoverItem>,
onClickItem: (Int, BaseItem) -> Unit, onClickItem: (Int, BaseItem) -> Unit,
overviewOnClick: () -> Unit, overviewOnClick: () -> Unit,
favoriteOnClick: () -> Unit, favoriteOnClick: () -> Unit,
onClickDiscover: (Int, DiscoverItem) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@ -336,8 +362,43 @@ fun PersonPageContent(
showIfEmpty = false, showIfEmpty = false,
horizontalPadding = 32.dp, horizontalPadding = 32.dp,
modifier = Modifier.fillMaxWidth(), 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,
)
}
}
} }
} }