From 0e195c18d74ae337546dea225424af4a17962294 Mon Sep 17 00:00:00 2001 From: damontecres <154766448+damontecres@users.noreply.github.com> Date: Thu, 16 Oct 2025 16:49:40 -0400 Subject: [PATCH] Show genres tab for TV & Movies (#16) Adds a tab to the TV Show & Movie screens which displays the genres in that library as a grid. You can select the genre to see the items in that library for the genre. This PR also adds backend changes for further filtering to implement #13. --- .../wholphin/data/model/GetItemsFilter.kt | 31 ++++ .../damontecres/wholphin/ui/Extensions.kt | 7 + .../damontecres/wholphin/ui/cards/GridCard.kt | 3 +- .../wholphin/ui/cards/SeasonCard.kt | 3 +- .../ui/components/CollectionFolderGrid.kt | 57 ++++---- .../wholphin/ui/components/GenreCardGrid.kt | 135 ++++++++++++++++++ .../wholphin/ui/detail/CardGrid.kt | 52 ++++--- .../ui/detail/CollectionFolderGeneric.kt | 12 +- .../ui/detail/CollectionFolderMovie.kt | 18 ++- .../wholphin/ui/detail/CollectionFolderTv.kt | 18 ++- .../wholphin/ui/nav/Destination.kt | 8 ++ .../wholphin/ui/nav/DestinationContent.kt | 22 ++- .../wholphin/util/ApiRequestPager.kt | 21 +++ 13 files changed, 330 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt 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 new file mode 100644 index 00000000..4a34d462 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt @@ -0,0 +1,31 @@ +@file:UseSerializers(UuidSerializer::class) + +package com.github.damontecres.wholphin.data.model + +import com.github.damontecres.wholphin.util.UuidSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import org.jellyfin.sdk.model.api.request.GetItemsRequest +import java.util.UUID + +@Serializable +data class GetItemsFilter( + val favorite: Boolean? = null, + val genres: List? = null, + val minCriticRating: Double? = null, + val persons: List? = null, + val played: Boolean? = null, + val studios: List? = null, + val tags: List? = null, +) { + fun applyTo(req: GetItemsRequest) = + req.copy( + isFavorite = favorite, + genreIds = genres, + minCriticRating = minCriticRating, + personIds = persons, + isPlayed = played, + studioIds = studios, + tags = tags, + ) +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt index 380d1420..42273280 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable @@ -313,6 +314,12 @@ fun rememberPosition(initialPosition: RowColumn = RowColumn(-1, -1)) = ) } +/** + * Convenient way to [rememberSaveable] a Int + */ +@Composable +fun rememberInt(initial: Int = 0) = rememberSaveable { mutableIntStateOf(initial) } + /** * Show a [Toast]. Ensures it runs on the main thread. */ diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/GridCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/GridCard.kt index 26d34ce6..39cb13f2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/GridCard.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/GridCard.kt @@ -38,6 +38,7 @@ fun GridCard( onLongClick: () -> Unit, modifier: Modifier = Modifier, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + imageAspectRatio: Float = 2f / 3f, ) { val dto = item?.data val focused by interactionSource.collectIsFocusedAsState() @@ -86,7 +87,7 @@ fun GridCard( modifier = Modifier .fillMaxWidth() - .aspectRatio(2f / 3f) // TODO + .aspectRatio(imageAspectRatio) .background(MaterialTheme.colorScheme.surfaceVariant), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/SeasonCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/SeasonCard.kt index 64a8762a..566d0d20 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/SeasonCard.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/SeasonCard.kt @@ -42,6 +42,7 @@ fun SeasonCard( imageWidth: Dp = Dp.Unspecified, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, showImageOverlay: Boolean = false, + aspectRatio: Float = item?.data?.primaryImageAspectRatio?.toFloat() ?: (2f / 3f), ) { val dto = item?.data val focused by interactionSource.collectIsFocusedAsState() @@ -62,7 +63,7 @@ fun SeasonCard( } else { focusedAfterDelay = false } - val aspectRatio = dto?.primaryImageAspectRatio?.toFloat() ?: (2f / 3f) +// val aspectRatio = dto?.primaryImageAspectRatio?.toFloat() ?: (2f / 3f) val width = imageHeight * aspectRatio val height = imageWidth * (1f / aspectRatio) Column( 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 308250b5..c771ce57 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 @@ -27,6 +27,7 @@ import androidx.lifecycle.viewModelScope import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.DefaultItemFields import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect @@ -36,7 +37,6 @@ import com.github.damontecres.wholphin.ui.data.SortAndDirection import com.github.damontecres.wholphin.ui.data.VideoSortOptions import com.github.damontecres.wholphin.ui.detail.CardGrid import com.github.damontecres.wholphin.ui.detail.ItemViewModel -import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.GetItemsRequestHandler @@ -66,12 +66,14 @@ class CollectionFolderViewModel val loading = MutableLiveData(LoadingState.Loading) val pager = MutableLiveData>(listOf()) val sortAndDirection = MutableLiveData() + val filter = MutableLiveData(GetItemsFilter()) fun init( itemId: UUID, potential: BaseItem?, sortAndDirection: SortAndDirection, recursive: Boolean, + filter: GetItemsFilter, ): Job = viewModelScope.launch( LoadingExceptionHandler( @@ -80,12 +82,13 @@ class CollectionFolderViewModel ) + Dispatchers.IO, ) { fetchItem(itemId, potential) - loadResults(sortAndDirection, recursive) + loadResults(sortAndDirection, recursive, filter) } fun loadResults( sortAndDirection: SortAndDirection, recursive: Boolean, + filter: GetItemsFilter, ) { item.value?.let { item -> viewModelScope.launch(Dispatchers.IO) { @@ -93,6 +96,7 @@ class CollectionFolderViewModel pager.value = listOf() loading.value = LoadingState.Loading this@CollectionFolderViewModel.sortAndDirection.value = sortAndDirection + this@CollectionFolderViewModel.filter.value = filter } val includeItemTypes = when (item.data.collectionType) { @@ -112,25 +116,27 @@ class CollectionFolderViewModel else -> listOf() } val request = - GetItemsRequest( - parentId = item.id, - enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB), - includeItemTypes = includeItemTypes, - recursive = recursive, - excludeItemIds = listOf(item.id), - sortBy = - listOf( - sortAndDirection.sort, - ItemSortBy.SORT_NAME, - ItemSortBy.PRODUCTION_YEAR, - ), - sortOrder = - listOf( - sortAndDirection.direction, - SortOrder.ASCENDING, - SortOrder.ASCENDING, - ), - fields = DefaultItemFields, + filter.applyTo( + GetItemsRequest( + parentId = item.id, + enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB), + includeItemTypes = includeItemTypes, + recursive = recursive, + excludeItemIds = listOf(item.id), + sortBy = + listOf( + sortAndDirection.sort, + ItemSortBy.SORT_NAME, + ItemSortBy.PRODUCTION_YEAR, + ), + sortOrder = + listOf( + sortAndDirection.direction, + SortOrder.ASCENDING, + SortOrder.ASCENDING, + ), + fields = DefaultItemFields, + ), ) val newPager = ApiRequestPager( @@ -182,7 +188,9 @@ class CollectionFolderViewModel @Composable fun CollectionFolderGrid( preferences: UserPreferences, - destination: Destination.MediaItem, + itemId: UUID, + item: BaseItem?, + initialFilter: GetItemsFilter, recursive: Boolean, onClickItem: (BaseItem) -> Unit, modifier: Modifier = Modifier, @@ -196,9 +204,10 @@ fun CollectionFolderGrid( positionCallback: ((columns: Int, position: Int) -> Unit)? = null, ) { OneTimeLaunchedEffect { - viewModel.init(destination.itemId, destination.item, initialSortAndDirection, recursive) + viewModel.init(itemId, item, initialSortAndDirection, recursive, initialFilter) } val sortAndDirection by viewModel.sortAndDirection.observeAsState(initialSortAndDirection) + val filter by viewModel.filter.observeAsState(initialFilter) val loading by viewModel.loading.observeAsState(LoadingState.Loading) val item by viewModel.item.observeAsState() val pager by viewModel.pager.observeAsState() @@ -218,7 +227,7 @@ fun CollectionFolderGrid( modifier = modifier, onClickItem = onClickItem, onSortChange = { - viewModel.loadResults(it, recursive) + viewModel.loadResults(it, recursive, filter) }, showTitle = showTitle, positionCallback = positionCallback, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt new file mode 100644 index 00000000..7ad6a5ec --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt @@ -0,0 +1,135 @@ +package com.github.damontecres.wholphin.ui.components + +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.GetItemsFilter +import com.github.damontecres.wholphin.ui.DefaultItemFields +import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect +import com.github.damontecres.wholphin.ui.cards.GridCard +import com.github.damontecres.wholphin.ui.detail.CardGrid +import com.github.damontecres.wholphin.ui.nav.Destination +import com.github.damontecres.wholphin.ui.nav.NavigationManager +import com.github.damontecres.wholphin.ui.tryRequestFocus +import com.github.damontecres.wholphin.util.ApiRequestPager +import com.github.damontecres.wholphin.util.GetGenresRequestHandler +import com.github.damontecres.wholphin.util.LoadingExceptionHandler +import com.github.damontecres.wholphin.util.LoadingState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.model.api.request.GetGenresRequest +import java.util.UUID +import javax.inject.Inject + +@HiltViewModel +class GenreViewModel + @Inject + constructor( + private val api: ApiClient, + val navigationManager: NavigationManager, + ) : ViewModel() { + private lateinit var itemId: UUID + val loading = MutableLiveData(LoadingState.Pending) + val genres = MutableLiveData>(listOf()) + + fun init(itemId: UUID) { + loading.value = LoadingState.Loading + this.itemId = itemId + viewModelScope.launch(Dispatchers.IO + LoadingExceptionHandler(loading, "Failed to fetch genres")) { + val request = + GetGenresRequest( + parentId = itemId, + fields = DefaultItemFields, + ) + val pager = ApiRequestPager(api, request, GetGenresRequestHandler, viewModelScope).init() + withContext(Dispatchers.Main) { + genres.value = pager + loading.value = LoadingState.Success + } + } + } + + suspend fun positionOfLetter(letter: Char): Int { + val request = + GetGenresRequest( + parentId = itemId, + nameLessThan = letter.toString(), + limit = 0, + enableTotalRecordCount = true, + ) + val result by GetGenresRequestHandler.execute(api, request) + return result.totalRecordCount + } + } + +@Composable +fun GenreCardGrid( + itemId: UUID, + modifier: Modifier = Modifier, + viewModel: GenreViewModel = hiltViewModel(), +) { + OneTimeLaunchedEffect { + viewModel.init(itemId) + } + val loading by viewModel.loading.observeAsState(LoadingState.Pending) + val genres by viewModel.genres.observeAsState(listOf()) + + val gridFocusRequester = remember { FocusRequester() } + when (val st = loading) { + LoadingState.Pending, + LoadingState.Loading, + -> LoadingPage(modifier.focusable()) + + is LoadingState.Error -> ErrorMessage(st, modifier.focusable()) + + LoadingState.Success -> + Box(modifier = modifier) { + LaunchedEffect(Unit) { gridFocusRequester.tryRequestFocus() } + CardGrid( + pager = genres, + onClickItem = { genre -> + viewModel.navigationManager.navigateTo( + Destination.FilteredCollection( + itemId = itemId, + filter = GetItemsFilter(genres = listOf(genre.id)), + recursive = true, + ), + ) + }, + onLongClickItem = {}, + letterPosition = { viewModel.positionOfLetter(it) }, + gridFocusRequester = gridFocusRequester, + showJumpButtons = false, + showLetterButtons = true, + modifier = Modifier.fillMaxSize(), + initialPosition = 0, + positionCallback = { columns, position -> + }, + cardContent = { item: BaseItem?, onClick: () -> Unit, onLongClick: () -> Unit, mod: Modifier -> + GridCard( + item = item, + onClick = onClick, + onLongClick = onLongClick, + modifier = mod, + imageAspectRatio = 1f, + ) + }, + ) + } + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt index f3a19f30..9a7d3d77 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt @@ -77,6 +77,19 @@ fun CardGrid( modifier: Modifier = Modifier, initialPosition: Int = 0, positionCallback: ((columns: Int, position: Int) -> Unit)? = null, + cardContent: @Composable ( + item: BaseItem?, + onClick: () -> Unit, + onLongClick: () -> Unit, + mod: Modifier, + ) -> Unit = { item, onClick, onLongClick, mod -> + GridCard( + item = item, + onClick = onClick, + onLongClick = onLongClick, + modifier = mod, + ) + }, ) { val startPosition = initialPosition.coerceIn(0, (pager.size - 1).coerceAtLeast(0)) val columns = 6 @@ -249,34 +262,33 @@ fun CardGrid( Modifier } val item = pager[index] - GridCard( - item = item, - onClick = { + cardContent( + item, + { if (item != null) { focusedIndex = index onClickItem.invoke(item) } }, - onLongClick = { if (item != null) onLongClickItem.invoke(item) }, - modifier = - mod - .ifElse(index == 0, Modifier.focusRequester(zeroFocus)) - .onFocusChanged { focusState -> - if (DEBUG) { - Timber.v( - "$index isFocused=${focusState.isFocused}", - ) - } - if (focusState.isFocused) { - // Focused, so set that up - focusOn(index) - positionCallback?.invoke(columns, index) - } else if (focusedIndex == index) { + { if (item != null) onLongClickItem.invoke(item) }, + mod + .ifElse(index == 0, Modifier.focusRequester(zeroFocus)) + .onFocusChanged { focusState -> + if (DEBUG) { + Timber.v( + "$index isFocused=${focusState.isFocused}", + ) + } + if (focusState.isFocused) { + // Focused, so set that up + focusOn(index) + positionCallback?.invoke(columns, index) + } else if (focusedIndex == index) { // savedFocusedIndex = index // // Was focused on this, so mark unfocused // focusedIndex = -1 - } - }, + } + }, ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderGeneric.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderGeneric.kt index 8279cddc..cbf0f3ee 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderGeneric.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderGeneric.kt @@ -9,24 +9,30 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid -import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel +import java.util.UUID @Composable fun CollectionFolderGeneric( preferences: UserPreferences, - destination: Destination.MediaItem, + itemId: UUID, + item: BaseItem?, recursive: Boolean, modifier: Modifier = Modifier, + filter: GetItemsFilter = GetItemsFilter(), preferencesViewModel: PreferencesViewModel = hiltViewModel(), ) { var showHeader by remember { mutableStateOf(true) } CollectionFolderGrid( preferences = preferences, onClickItem = { preferencesViewModel.navigationManager.navigateTo(it.destination()) }, - destination = destination, + itemId = itemId, + item = item, + initialFilter = filter, showTitle = showHeader, recursive = recursive, modifier = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt index d0d1868d..ea30bb9f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt @@ -30,10 +30,12 @@ import androidx.tv.material3.TabRow import androidx.tv.material3.TabRowDefaults import androidx.tv.material3.Text import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.rememberTab import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid import com.github.damontecres.wholphin.ui.components.ErrorMessage +import com.github.damontecres.wholphin.ui.components.GenreCardGrid import com.github.damontecres.wholphin.ui.components.RecommendedMovie import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.nav.Destination @@ -55,7 +57,7 @@ fun CollectionFolderMovie( 0 } - val tabs = listOf("Recommended", "Library") + val tabs = listOf("Recommended", "Library", "Genres") var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } val focusRequester = remember { FocusRequester() } @@ -154,7 +156,9 @@ fun CollectionFolderMovie( CollectionFolderGrid( preferences = preferences, onClickItem = onClickItem, - destination = destination, + itemId = destination.itemId, + item = destination.item, + initialFilter = GetItemsFilter(), showTitle = false, recursive = true, modifier = @@ -167,6 +171,16 @@ fun CollectionFolderMovie( }, ) } + 2 -> { + GenreCardGrid( + itemId = destination.itemId, + modifier = + Modifier + .padding(start = 16.dp) + .fillMaxSize() + .focusRequester(focusRequester), + ) + } else -> ErrorMessage("Invalid tab index $selectedTabIndex", null) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt index 048d6d71..a6e5c89a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt @@ -30,10 +30,12 @@ import androidx.tv.material3.TabRow import androidx.tv.material3.TabRowDefaults import androidx.tv.material3.Text import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.rememberTab import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid import com.github.damontecres.wholphin.ui.components.ErrorMessage +import com.github.damontecres.wholphin.ui.components.GenreCardGrid import com.github.damontecres.wholphin.ui.components.RecommendedTvShow import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.nav.Destination @@ -55,7 +57,7 @@ fun CollectionFolderTv( 0 } - val tabs = listOf("Recommended", "Library") + val tabs = listOf("Recommended", "Library", "Genres") var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } val focusRequester = remember { FocusRequester() } @@ -153,7 +155,9 @@ fun CollectionFolderTv( 1 -> { CollectionFolderGrid( preferences = preferences, - destination = destination, + itemId = destination.itemId, + item = destination.item, + initialFilter = GetItemsFilter(), showTitle = false, recursive = true, modifier = @@ -167,6 +171,16 @@ fun CollectionFolderTv( onClickItem = onClickItem, ) } + 2 -> { + GenreCardGrid( + itemId = destination.itemId, + modifier = + Modifier + .padding(start = 16.dp) + .fillMaxSize() + .focusRequester(focusRequester), + ) + } else -> ErrorMessage("Invalid tab index $selectedTabIndex", null) } } 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 0565817d..250aa9ab 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 @@ -4,6 +4,7 @@ package com.github.damontecres.wholphin.ui.nav import androidx.navigation3.runtime.NavKey import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption import com.github.damontecres.wholphin.util.UuidSerializer @@ -75,6 +76,13 @@ sealed class Destination( constructor(item: BaseItem) : this(item.id, item.resumeMs ?: 0, item) } + @Serializable + data class FilteredCollection( + val itemId: UUID, + val filter: GetItemsFilter, + val recursive: Boolean, + ) : Destination(false) + @Serializable data object UpdateApp : Destination(true) 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 b1d4db9d..e19b9819 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 @@ -93,7 +93,8 @@ fun DestinationContent( BaseItemKind.BOX_SET -> CollectionFolderGeneric( preferences, - destination, + destination.itemId, + destination.item, false, modifier, ) @@ -117,7 +118,8 @@ fun DestinationContent( CollectionType.BOXSETS -> CollectionFolderGeneric( preferences, - destination, + destination.itemId, + destination.item, true, modifier, ) @@ -125,7 +127,8 @@ fun DestinationContent( else -> CollectionFolderGeneric( preferences, - destination, + destination.itemId, + destination.item, false, modifier, ) @@ -141,7 +144,8 @@ fun DestinationContent( BaseItemKind.USER_VIEW -> CollectionFolderGeneric( preferences, - destination, + destination.itemId, + destination.item, true, modifier, ) @@ -151,6 +155,16 @@ fun DestinationContent( } } + is Destination.FilteredCollection -> + CollectionFolderGeneric( + preferences = preferences, + itemId = destination.itemId, + item = null, + filter = destination.filter, + recursive = destination.recursive, + modifier = modifier, + ) + Destination.UpdateApp -> InstallUpdatePage(preferences, modifier) Destination.License -> LicenseInfo(modifier) 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 754dad46..7c8cd69d 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 @@ -15,12 +15,14 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.jellyfin.sdk.api.client.ApiClient 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.playlistsApi import org.jellyfin.sdk.api.client.extensions.suggestionsApi import org.jellyfin.sdk.api.client.extensions.tvShowsApi import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult 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.GetPlaylistItemsRequest @@ -292,3 +294,22 @@ val GetPlaylistItemsRequestHandler = request: GetPlaylistItemsRequest, ): Response = api.playlistsApi.getPlaylistItems(request) } + +val GetGenresRequestHandler = + object : RequestHandler { + override fun prepare( + request: GetGenresRequest, + startIndex: Int, + limit: Int, + enableTotalRecordCount: Boolean, + ): GetGenresRequest = + request.copy( + startIndex = startIndex, + limit = limit, + ) + + override suspend fun execute( + api: ApiClient, + request: GetGenresRequest, + ): Response = api.genresApi.getGenres(request) + }