mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
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.
This commit is contained in:
parent
6a896cf600
commit
0e195c18d7
13 changed files with 330 additions and 57 deletions
|
|
@ -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<UUID>? = null,
|
||||
val minCriticRating: Double? = null,
|
||||
val persons: List<UUID>? = null,
|
||||
val played: Boolean? = null,
|
||||
val studios: List<UUID>? = null,
|
||||
val tags: List<String>? = null,
|
||||
) {
|
||||
fun applyTo(req: GetItemsRequest) =
|
||||
req.copy(
|
||||
isFavorite = favorite,
|
||||
genreIds = genres,
|
||||
minCriticRating = minCriticRating,
|
||||
personIds = persons,
|
||||
isPlayed = played,
|
||||
studioIds = studios,
|
||||
tags = tags,
|
||||
)
|
||||
}
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>(LoadingState.Loading)
|
||||
val pager = MutableLiveData<List<BaseItem?>>(listOf())
|
||||
val sortAndDirection = MutableLiveData<SortAndDirection>()
|
||||
val filter = MutableLiveData<GetItemsFilter>(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,
|
||||
|
|
|
|||
|
|
@ -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>(LoadingState.Pending)
|
||||
val genres = MutableLiveData<List<BaseItem?>>(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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<BaseItemDtoQueryResult> = api.playlistsApi.getPlaylistItems(request)
|
||||
}
|
||||
|
||||
val GetGenresRequestHandler =
|
||||
object : RequestHandler<GetGenresRequest> {
|
||||
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<BaseItemDtoQueryResult> = api.genresApi.getGenres(request)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue