diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt index 09acd57e..6330113e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt @@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.request.GetItemsRequest +import org.jellyfin.sdk.model.api.request.GetPersonsRequest import org.jellyfin.sdk.model.serializer.UUIDSerializer import java.util.UUID @@ -19,6 +20,7 @@ data class GetItemsFilter( val studios: List? = null, val tags: List? = null, val includeItemTypes: List? = null, + val override: GetItemsFilterOverride = GetItemsFilterOverride.NONE, ) { fun applyTo(req: GetItemsRequest) = req.copy( @@ -31,4 +33,14 @@ data class GetItemsFilter( studioIds = studios, tags = tags, ) + + fun applyTo(req: GetPersonsRequest) = + req.copy( + isFavorite = favorite, + ) +} + +enum class GetItemsFilterOverride { + NONE, + PERSON, } 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 398d333d..3430640e 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 @@ -13,6 +13,7 @@ data class Person( val role: String?, val type: PersonKind, val imageUrl: String?, + val favorite: Boolean, ) { companion object { fun fromDto( @@ -25,6 +26,21 @@ data class Person( role = dto.role, type = dto.type, imageUrl = api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY), + favorite = false, + ) + + fun fromDto( + dto: BaseItemPerson, + favorite: Boolean, + api: ApiClient, + ): Person = + Person( + id = dto.id, + name = dto.name, + role = dto.role, + type = dto.type, + imageUrl = api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY), + favorite = favorite, ) } } 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 07545a53..dcde2ff0 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 @@ -72,8 +72,8 @@ fun PersonCard( ItemCardImage( imageUrl = item.imageUrl, name = item.name, - showOverlay = false, - favorite = false, + showOverlay = true, + favorite = item.favorite, watched = false, unwatchedCount = -1, watchedPercent = null, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt index 2426c44a..53a4988a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt @@ -34,6 +34,7 @@ import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.GetItemsFilter +import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect @@ -53,6 +54,7 @@ import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.FavoriteWatchManager import com.github.damontecres.wholphin.util.GetItemsRequestHandler +import com.github.damontecres.wholphin.util.GetPersonsHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import dagger.hilt.android.lifecycle.HiltViewModel @@ -69,6 +71,7 @@ import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.request.GetItemsRequest +import org.jellyfin.sdk.model.api.request.GetPersonsRequest import org.jellyfin.sdk.model.serializer.toUUIDOrNull import java.util.UUID import javax.inject.Inject @@ -107,13 +110,12 @@ class CollectionFolderViewModel } val sortAndDirection = - if (initialSortAndDirection == null) { - serverRepository.currentUser.value?.let { user -> - libraryDisplayInfoDao.getItem(user, itemId)?.sortAndDirection - } ?: SortAndDirection.DEFAULT - } else { - SortAndDirection.DEFAULT - } + initialSortAndDirection + ?: ( + serverRepository.currentUser.value?.let { user -> + libraryDisplayInfoDao.getItem(user, itemId)?.sortAndDirection + } ?: SortAndDirection.DEFAULT + ) loadResults(sortAndDirection, recursive, filter) } @@ -143,7 +145,6 @@ class CollectionFolderViewModel recursive: Boolean, filter: GetItemsFilter, ) { - val item = item.value viewModelScope.launch(Dispatchers.IO) { withContext(Dispatchers.Main) { pager.value = listOf() @@ -151,62 +152,7 @@ class CollectionFolderViewModel this@CollectionFolderViewModel.sortAndDirection.value = sortAndDirection this@CollectionFolderViewModel.filter.value = filter } - val includeItemTypes = - when (item?.data?.collectionType) { - CollectionType.MOVIES -> listOf(BaseItemKind.MOVIE) - CollectionType.TVSHOWS -> listOf(BaseItemKind.SERIES) - CollectionType.HOMEVIDEOS -> listOf(BaseItemKind.VIDEO) - CollectionType.MUSIC -> - listOf( - BaseItemKind.AUDIO, - BaseItemKind.MUSIC_ARTIST, - BaseItemKind.MUSIC_ALBUM, - ) - - CollectionType.BOXSETS -> listOf(BaseItemKind.BOX_SET) - CollectionType.PLAYLISTS -> listOf(BaseItemKind.PLAYLIST) - - else -> listOf() - } - val request = - filter.applyTo( - GetItemsRequest( - parentId = item?.id, - enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB), - includeItemTypes = includeItemTypes, - recursive = recursive, - excludeItemIds = item?.let { listOf(item.id) }, - sortBy = - buildList { - add(sortAndDirection.sort) - if (sortAndDirection.sort != ItemSortBy.SORT_NAME) { - add(ItemSortBy.SORT_NAME) - } - if (item?.data?.collectionType == CollectionType.MOVIES) { - add(ItemSortBy.PRODUCTION_YEAR) - } - }, - sortOrder = - buildList { - add(sortAndDirection.direction) - if (sortAndDirection.sort != ItemSortBy.SORT_NAME) { - add(SortOrder.ASCENDING) - } - if (item?.data?.collectionType == CollectionType.MOVIES) { - add(SortOrder.ASCENDING) - } - }, - fields = SlimItemFields, - ), - ) - val newPager = - ApiRequestPager( - api, - request, - GetItemsRequestHandler, - viewModelScope, - useSeriesForPrimary = true, - ) + val newPager = createPager(sortAndDirection, recursive, filter) newPager.init() if (newPager.isNotEmpty()) newPager.getBlocking(0) withContext(Dispatchers.Main) { @@ -216,6 +162,93 @@ class CollectionFolderViewModel } } + private fun createPager( + sortAndDirection: SortAndDirection, + recursive: Boolean, + filter: GetItemsFilter, + ): ApiRequestPager { + val item = item.value + return when (filter.override) { + GetItemsFilterOverride.NONE -> { + val includeItemTypes = + when (item?.data?.collectionType) { + CollectionType.MOVIES -> listOf(BaseItemKind.MOVIE) + CollectionType.TVSHOWS -> listOf(BaseItemKind.SERIES) + CollectionType.HOMEVIDEOS -> listOf(BaseItemKind.VIDEO) + CollectionType.MUSIC -> + listOf( + BaseItemKind.AUDIO, + BaseItemKind.MUSIC_ARTIST, + BaseItemKind.MUSIC_ALBUM, + ) + + CollectionType.BOXSETS -> listOf(BaseItemKind.BOX_SET) + CollectionType.PLAYLISTS -> listOf(BaseItemKind.PLAYLIST) + + else -> listOf() + } + val request = + filter.applyTo( + GetItemsRequest( + parentId = item?.id, + enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB), + includeItemTypes = includeItemTypes, + recursive = recursive, + excludeItemIds = item?.let { listOf(item.id) }, + sortBy = + buildList { + add(sortAndDirection.sort) + if (sortAndDirection.sort != ItemSortBy.SORT_NAME) { + add(ItemSortBy.SORT_NAME) + } + if (item?.data?.collectionType == CollectionType.MOVIES) { + add(ItemSortBy.PRODUCTION_YEAR) + } + }, + sortOrder = + buildList { + add(sortAndDirection.direction) + if (sortAndDirection.sort != ItemSortBy.SORT_NAME) { + add(SortOrder.ASCENDING) + } + if (item?.data?.collectionType == CollectionType.MOVIES) { + add(SortOrder.ASCENDING) + } + }, + fields = SlimItemFields, + ), + ) + val newPager = + ApiRequestPager( + api, + request, + GetItemsRequestHandler, + viewModelScope, + useSeriesForPrimary = true, + ) + newPager + } + + GetItemsFilterOverride.PERSON -> { + val request = + filter.applyTo( + GetPersonsRequest( + enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB), + ), + ) + val newPager = + ApiRequestPager( + api, + request, + GetPersonsHandler, + viewModelScope, + useSeriesForPrimary = true, + ) + newPager + } + } + } + suspend fun positionOfLetter(letter: Char): Int? = withContext(Dispatchers.IO) { item.value?.let { item -> @@ -437,12 +470,14 @@ fun CollectionFolderGridContent( modifier = Modifier.fillMaxWidth(), ) } - SortByButton( - sortOptions = sortOptions, - current = sortAndDirection, - onSortChange = onSortChange, - modifier = Modifier, - ) + if (sortOptions.isNotEmpty()) { + SortByButton( + sortOptions = sortOptions, + current = sortAndDirection, + onSortChange = onSortChange, + modifier = Modifier, + ) + } } CardGrid( pager = pager, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/data/SortAndDirection.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/data/SortAndDirection.kt index 3cb6635b..634be82f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/data/SortAndDirection.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/data/SortAndDirection.kt @@ -79,5 +79,6 @@ fun getStringRes(sort: ItemSortBy): Int = ItemSortBy.CRITIC_RATING -> R.string.critic_rating ItemSortBy.PLAY_COUNT -> R.string.play_count ItemSortBy.AIRED_EPISODE_ORDER -> R.string.aired_episode_order + ItemSortBy.DEFAULT -> R.string.default_track else -> throw IllegalArgumentException("Unsupported sort option: $sort") } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt index 5fabe479..d62d0f73 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt @@ -288,7 +288,6 @@ fun buildMoreDialogItemsForHome( fun buildMoreDialogItemsForPerson( context: Context, person: Person, -// favorite: Boolean, actions: MoreDialogActions, ): List = buildList { @@ -301,14 +300,13 @@ fun buildMoreDialogItemsForPerson( actions.navigateTo(Destination.MediaItem(itemId, BaseItemKind.PERSON)) }, ) - // TODO need way to get person's favorite status -// add( -// DialogItem( -// text = if (favorite) R.string.remove_favorite else R.string.add_favorite, -// iconStringRes = R.string.fa_heart, -// iconColor = if (favorite) Color.Red else Color.Unspecified, -// ) { -// actions.onClickFavorite.invoke(itemId, !favorite) -// }, -// ) + add( + DialogItem( + text = if (person.favorite) R.string.remove_favorite else R.string.add_favorite, + iconStringRes = R.string.fa_heart, + iconColor = if (person.favorite) Color.Red else Color.Unspecified, + ) { + actions.onClickFavorite.invoke(itemId, !person.favorite) + }, + ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt index 0afa076d..dac2b915 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt @@ -25,6 +25,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.GetItemsFilter +import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid import com.github.damontecres.wholphin.ui.components.ErrorMessage @@ -32,11 +33,15 @@ import com.github.damontecres.wholphin.ui.components.TabRow import com.github.damontecres.wholphin.ui.data.EpisodeSortOptions import com.github.damontecres.wholphin.ui.data.MovieSortOptions import com.github.damontecres.wholphin.ui.data.SeriesSortOptions +import com.github.damontecres.wholphin.ui.data.SortAndDirection +import com.github.damontecres.wholphin.ui.data.VideoSortOptions import com.github.damontecres.wholphin.ui.logTab import com.github.damontecres.wholphin.ui.nav.NavDrawerItem import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel import com.github.damontecres.wholphin.ui.tryRequestFocus import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.ItemSortBy +import org.jellyfin.sdk.model.api.SortOrder @Composable fun FavoritesPage( @@ -59,6 +64,9 @@ fun FavoritesPage( stringResource(R.string.movies), stringResource(R.string.tv_shows), stringResource(R.string.episodes), + stringResource(R.string.videos), + stringResource(R.string.playlists), + stringResource(R.string.people), ) var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } val focusRequester = remember { FocusRequester() } @@ -165,6 +173,81 @@ fun FavoritesPage( }, ) } + 3 -> { + CollectionFolderGrid( + preferences = preferences, + onClickItem = { _, item -> onClickItem.invoke(item) }, + itemId = "${NavDrawerItem.Favorites.id}_videos", + initialFilter = + GetItemsFilter( + favorite = true, + includeItemTypes = listOf(BaseItemKind.VIDEO), + ), + showTitle = false, + recursive = true, + sortOptions = VideoSortOptions, + modifier = + Modifier + .padding(start = 16.dp) + .fillMaxSize() + .focusRequester(focusRequester), + positionCallback = { columns, position -> + showHeader = position < columns + }, + ) + } + 4 -> { + CollectionFolderGrid( + preferences = preferences, + onClickItem = { _, item -> onClickItem.invoke(item) }, + itemId = "${NavDrawerItem.Favorites.id}_playlists", + initialFilter = + GetItemsFilter( + favorite = true, + includeItemTypes = listOf(BaseItemKind.PLAYLIST), + ), + showTitle = false, + recursive = true, + sortOptions = VideoSortOptions, + modifier = + Modifier + .padding(start = 16.dp) + .fillMaxSize() + .focusRequester(focusRequester), + positionCallback = { columns, position -> + showHeader = position < columns + }, + ) + } + + 5 -> { + CollectionFolderGrid( + preferences = preferences, + onClickItem = { _, item -> onClickItem.invoke(item) }, + itemId = "${NavDrawerItem.Favorites.id}_people", + initialFilter = + GetItemsFilter( + favorite = true, + override = GetItemsFilterOverride.PERSON, + ), + initialSortAndDirection = + SortAndDirection( + ItemSortBy.DEFAULT, + SortOrder.ASCENDING, + ), + showTitle = false, + recursive = true, + sortOptions = listOf(), + modifier = + Modifier + .padding(start = 16.dp) + .fillMaxSize() + .focusRequester(focusRequester), + positionCallback = { columns, position -> + showHeader = position < columns + }, + ) + } else -> ErrorMessage("Invalid tab index $selectedTabIndex", null) } } 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 db2ff356..d89e71d8 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 @@ -26,6 +26,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -43,11 +45,13 @@ 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.components.ErrorMessage +import com.github.damontecres.wholphin.ui.components.ExpandableFaButton import com.github.damontecres.wholphin.ui.components.LoadingPage 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.isNotNullOrBlank import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.NavigationManager @@ -56,6 +60,7 @@ 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.FavoriteWatchManager import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState @@ -78,6 +83,7 @@ class PersonViewModel constructor( api: ApiClient, val navigationManager: NavigationManager, + private val favoriteWatchManager: FavoriteWatchManager, ) : LoadingItemViewModel(api) { val movies = MutableLiveData(RowLoadingState.Pending) val series = MutableLiveData(RowLoadingState.Pending) @@ -144,6 +150,13 @@ class PersonViewModel } } } + + fun setFavorite(favorite: Boolean) { + viewModelScope.launchIO { + favoriteWatchManager.setFavorite(itemUuid, favorite) + fetchAndSetItem(itemUuid) + } + } } @Composable @@ -180,6 +193,7 @@ fun PersonPage( birthdate = person.data.premiereDate?.toLocalDate(), deathdate = person.data.endDate?.toLocalDate(), birthPlace = person.data.productionLocations?.firstOrNull(), + favorite = person.favorite, movies = movies, series = series, episodes = episodes, @@ -187,6 +201,9 @@ fun PersonPage( viewModel.navigationManager.navigateTo(item.destination()) }, overviewOnClick = { showOverviewDialog = true }, + favoriteOnClick = { + viewModel.setFavorite(!person.favorite) + }, modifier = modifier, ) AnimatedVisibility(showOverviewDialog) { @@ -220,11 +237,13 @@ fun PersonPageContent( birthdate: LocalDate?, deathdate: LocalDate?, birthPlace: String?, + favorite: Boolean, movies: RowLoadingState, series: RowLoadingState, episodes: RowLoadingState, onClickItem: (Int, BaseItem) -> Unit, overviewOnClick: () -> Unit, + favoriteOnClick: () -> Unit, modifier: Modifier = Modifier, ) { val focusRequester = remember { FocusRequester() } @@ -237,8 +256,10 @@ fun PersonPageContent( headerFocusRequester.tryRequestFocus() } } + var focusedOnHeader by remember { mutableStateOf(true) } LazyColumn( verticalArrangement = Arrangement.spacedBy(16.dp), + userScrollEnabled = !focusedOnHeader, modifier = modifier .padding(start = 16.dp), @@ -251,13 +272,18 @@ fun PersonPageContent( birthdate = birthdate, birthPlace = birthPlace, deathdate = deathdate, + favorite = favorite, overviewOnClick = overviewOnClick, + favoriteOnClick = favoriteOnClick, modifier = Modifier .fillMaxWidth() .heightIn(max = 480.dp) .padding(top = 16.dp, bottom = 40.dp) - .focusRequester(headerFocusRequester), + .focusRequester(headerFocusRequester) + .onFocusChanged { + focusedOnHeader = it.hasFocus + }, ) } item { @@ -321,7 +347,9 @@ fun PersonHeader( birthdate: LocalDate?, deathdate: LocalDate?, birthPlace: String?, + favorite: Boolean, overviewOnClick: () -> Unit, + favoriteOnClick: () -> Unit, modifier: Modifier = Modifier, ) { val bringIntoViewRequester = remember { BringIntoViewRequester() } @@ -380,9 +408,9 @@ fun PersonHeader( overflow = TextOverflow.Ellipsis, ) } - birthPlace?.let { + if (birthPlace.isNotNullOrBlank()) { Text( - text = stringResource(R.string.birthplace) + ": $it", + text = stringResource(R.string.birthplace) + ": $birthPlace", color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.titleSmall, maxLines = 1, @@ -411,9 +439,12 @@ fun PersonHeader( ) } val interactionSource = remember { MutableInteractionSource() } - val focused by interactionSource.collectIsFocusedAsState() - if (focused) { - LaunchedEffect(Unit) { + val buttonInteractionSource = remember { MutableInteractionSource() } + val focused = + interactionSource.collectIsFocusedAsState().value || + buttonInteractionSource.collectIsFocusedAsState().value + LaunchedEffect(focused) { + if (focused) { bringIntoViewRequester.bringIntoView() } } @@ -424,6 +455,14 @@ fun PersonHeader( interactionSource = interactionSource, modifier = Modifier.padding(top = 8.dp), ) + ExpandableFaButton( + title = if (favorite) R.string.remove_favorite else R.string.add_favorite, + iconStringRes = R.string.fa_heart, + onClick = favoriteOnClick, + iconColor = if (favorite) Color.Red else Color.Unspecified, + interactionSource = buttonInteractionSource, + modifier = Modifier, + ) } } } @@ -440,6 +479,8 @@ private fun PersonPreview() { birthPlace = "Phoenix, Arizona, USA", deathdate = LocalDate.of(2025, 2, 1), overviewOnClick = {}, + favorite = true, + favoriteOnClick = {}, modifier = Modifier.fillMaxWidth(), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt index 51c7dafa..0c29ae3c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt @@ -18,7 +18,6 @@ import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.preferences.ThemeSongVolume import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.launchIO -import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.setValueOnMain @@ -26,6 +25,7 @@ import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.FavoriteWatchManager import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState +import com.github.damontecres.wholphin.util.PeopleFavorites import com.github.damontecres.wholphin.util.ThemeSongPlayer import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -56,6 +56,7 @@ class MovieViewModel val itemPlaybackRepository: ItemPlaybackRepository, private val themeSongPlayer: ThemeSongPlayer, private val favoriteWatchManager: FavoriteWatchManager, + private val peopleFavorites: PeopleFavorites, @Assisted val itemId: UUID, ) : ViewModel() { @AssistedFactory @@ -134,12 +135,11 @@ class MovieViewModel this@MovieViewModel.trailers.value = localTrailers + remoteTrailers } } + viewModelScope.launchIO { + val people = peopleFavorites.getPeopleFor(item) + this@MovieViewModel.people.setValueOnMain(people) + } withContext(Dispatchers.Main) { - people.value = - item.data.people - ?.letNotEmpty { people -> - people.map { Person.fromDto(it, api) } - }.orEmpty() chapters.value = Chapter.fromDto(item.data, api) } if (!similar.isInitialized) { @@ -171,7 +171,14 @@ class MovieViewModel favorite: Boolean, ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { favoriteWatchManager.setFavorite(itemId, favorite) + val item = item.value fetchAndSetItem() + if (item != null && itemId != item.id) { + viewModelScope.launchIO { + val people = peopleFavorites.getPeopleFor(item) + this@MovieViewModel.people.setValueOnMain(people) + } + } } fun savePlayVersion( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt index 33a5e476..43ca6f55 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt @@ -16,7 +16,6 @@ import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.detail.ItemViewModel import com.github.damontecres.wholphin.ui.equalsNotNull import com.github.damontecres.wholphin.ui.launchIO -import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.setValueOnMain @@ -28,6 +27,7 @@ import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler 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.PeopleFavorites import com.github.damontecres.wholphin.util.ThemeSongPlayer import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -61,6 +61,7 @@ class SeriesViewModel private val itemPlaybackRepository: ItemPlaybackRepository, private val themeSongPlayer: ThemeSongPlayer, private val favoriteWatchManager: FavoriteWatchManager, + private val peopleFavorites: PeopleFavorites, ) : ItemViewModel(api) { private lateinit var seriesId: UUID private lateinit var prefs: UserPreferences @@ -109,11 +110,10 @@ class SeriesViewModel this@SeriesViewModel.seasons.value = seasons episodes.value = episodeInfo loading.value = LoadingState.Success - people.value = - item.data.people - ?.letNotEmpty { people -> - people.map { Person.fromDto(it, api) } - }.orEmpty() + } + viewModelScope.launchIO { + val people = peopleFavorites.getPeopleFor(item) + this@SeriesViewModel.people.setValueOnMain(people) } if (!similar.isInitialized) { viewModelScope.launchIO { @@ -255,7 +255,11 @@ class SeriesViewModel if (listIndex != null) { refreshEpisode(itemId, listIndex) } else { - fetchItem(seriesId) + val item = fetchItem(seriesId) + viewModelScope.launchIO { + val people = peopleFavorites.getPeopleFor(item) + this@SeriesViewModel.people.setValueOnMain(people) + } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt b/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt index 2cc5bb3d..3156bb3d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt @@ -18,6 +18,7 @@ import org.jellyfin.sdk.api.client.Response import org.jellyfin.sdk.api.client.extensions.genresApi import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.api.client.extensions.liveTvApi +import org.jellyfin.sdk.api.client.extensions.personsApi import org.jellyfin.sdk.api.client.extensions.playlistsApi import org.jellyfin.sdk.api.client.extensions.suggestionsApi import org.jellyfin.sdk.api.client.extensions.tvShowsApi @@ -28,6 +29,7 @@ import org.jellyfin.sdk.model.api.request.GetEpisodesRequest import org.jellyfin.sdk.model.api.request.GetGenresRequest import org.jellyfin.sdk.model.api.request.GetItemsRequest import org.jellyfin.sdk.model.api.request.GetNextUpRequest +import org.jellyfin.sdk.model.api.request.GetPersonsRequest import org.jellyfin.sdk.model.api.request.GetPlaylistItemsRequest import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest import org.jellyfin.sdk.model.api.request.GetSuggestionsRequest @@ -365,3 +367,22 @@ val GetProgramsDtoHandler = request: GetProgramsDto, ): Response = api.liveTvApi.getPrograms(request) } + +val GetPersonsHandler = + object : RequestHandler { + override fun prepare( + request: GetPersonsRequest, + startIndex: Int, + limit: Int, + enableTotalRecordCount: Boolean, + ): GetPersonsRequest = + request.copy( +// startIndex = startIndex, + limit = limit, + ) + + override suspend fun execute( + api: ApiClient, + request: GetPersonsRequest, + ): Response = api.personsApi.getPersons((request)) + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/PeopleFavorites.kt b/app/src/main/java/com/github/damontecres/wholphin/util/PeopleFavorites.kt new file mode 100644 index 00000000..0ca4f0ba --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/util/PeopleFavorites.kt @@ -0,0 +1,34 @@ +package com.github.damontecres.wholphin.util + +import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.Person +import com.github.damontecres.wholphin.ui.letNotEmpty +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.api.client.extensions.itemsApi +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PeopleFavorites + @Inject + constructor( + private val api: ApiClient, + ) { + suspend fun getPeopleFor(item: BaseItem): List = + item.data.people.orEmpty().map { it.id }.chunked(50).let { chunks -> + val favorites = + chunks + .map { + api.itemsApi + .getItems(ids = it) + .content.items + }.flatten() + .associateBy({ it.id }, { it.userData?.isFavorite ?: false }) + val people = + item.data.people + ?.letNotEmpty { people -> + people.map { Person.fromDto(it, favorites[it.id] ?: false, api) } + }.orEmpty() + people + } + } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ac425f5e..53eb9b05 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -82,6 +82,7 @@ Playback Speed Playback Playlist + Playlists Preview User Profile Settings Queue @@ -135,6 +136,7 @@ Version Video Scale Video + Videos Watch live %1$d years old Play with transcoding