mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 08:01:20 +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.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
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.
|
* Show a [Toast]. Ensures it runs on the main thread.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ fun GridCard(
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
imageAspectRatio: Float = 2f / 3f,
|
||||||
) {
|
) {
|
||||||
val dto = item?.data
|
val dto = item?.data
|
||||||
val focused by interactionSource.collectIsFocusedAsState()
|
val focused by interactionSource.collectIsFocusedAsState()
|
||||||
|
|
@ -86,7 +87,7 @@ fun GridCard(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(2f / 3f) // TODO
|
.aspectRatio(imageAspectRatio)
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ fun SeasonCard(
|
||||||
imageWidth: Dp = Dp.Unspecified,
|
imageWidth: Dp = Dp.Unspecified,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
showImageOverlay: Boolean = false,
|
showImageOverlay: Boolean = false,
|
||||||
|
aspectRatio: Float = item?.data?.primaryImageAspectRatio?.toFloat() ?: (2f / 3f),
|
||||||
) {
|
) {
|
||||||
val dto = item?.data
|
val dto = item?.data
|
||||||
val focused by interactionSource.collectIsFocusedAsState()
|
val focused by interactionSource.collectIsFocusedAsState()
|
||||||
|
|
@ -62,7 +63,7 @@ fun SeasonCard(
|
||||||
} else {
|
} else {
|
||||||
focusedAfterDelay = false
|
focusedAfterDelay = false
|
||||||
}
|
}
|
||||||
val aspectRatio = dto?.primaryImageAspectRatio?.toFloat() ?: (2f / 3f)
|
// val aspectRatio = dto?.primaryImageAspectRatio?.toFloat() ?: (2f / 3f)
|
||||||
val width = imageHeight * aspectRatio
|
val width = imageHeight * aspectRatio
|
||||||
val height = imageWidth * (1f / aspectRatio)
|
val height = imageWidth * (1f / aspectRatio)
|
||||||
Column(
|
Column(
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import androidx.lifecycle.viewModelScope
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
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.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
||||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
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.data.VideoSortOptions
|
||||||
import com.github.damontecres.wholphin.ui.detail.CardGrid
|
import com.github.damontecres.wholphin.ui.detail.CardGrid
|
||||||
import com.github.damontecres.wholphin.ui.detail.ItemViewModel
|
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.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
|
|
@ -66,12 +66,14 @@ class CollectionFolderViewModel
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||||
val pager = MutableLiveData<List<BaseItem?>>(listOf())
|
val pager = MutableLiveData<List<BaseItem?>>(listOf())
|
||||||
val sortAndDirection = MutableLiveData<SortAndDirection>()
|
val sortAndDirection = MutableLiveData<SortAndDirection>()
|
||||||
|
val filter = MutableLiveData<GetItemsFilter>(GetItemsFilter())
|
||||||
|
|
||||||
fun init(
|
fun init(
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
potential: BaseItem?,
|
potential: BaseItem?,
|
||||||
sortAndDirection: SortAndDirection,
|
sortAndDirection: SortAndDirection,
|
||||||
recursive: Boolean,
|
recursive: Boolean,
|
||||||
|
filter: GetItemsFilter,
|
||||||
): Job =
|
): Job =
|
||||||
viewModelScope.launch(
|
viewModelScope.launch(
|
||||||
LoadingExceptionHandler(
|
LoadingExceptionHandler(
|
||||||
|
|
@ -80,12 +82,13 @@ class CollectionFolderViewModel
|
||||||
) + Dispatchers.IO,
|
) + Dispatchers.IO,
|
||||||
) {
|
) {
|
||||||
fetchItem(itemId, potential)
|
fetchItem(itemId, potential)
|
||||||
loadResults(sortAndDirection, recursive)
|
loadResults(sortAndDirection, recursive, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadResults(
|
fun loadResults(
|
||||||
sortAndDirection: SortAndDirection,
|
sortAndDirection: SortAndDirection,
|
||||||
recursive: Boolean,
|
recursive: Boolean,
|
||||||
|
filter: GetItemsFilter,
|
||||||
) {
|
) {
|
||||||
item.value?.let { item ->
|
item.value?.let { item ->
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
|
@ -93,6 +96,7 @@ class CollectionFolderViewModel
|
||||||
pager.value = listOf()
|
pager.value = listOf()
|
||||||
loading.value = LoadingState.Loading
|
loading.value = LoadingState.Loading
|
||||||
this@CollectionFolderViewModel.sortAndDirection.value = sortAndDirection
|
this@CollectionFolderViewModel.sortAndDirection.value = sortAndDirection
|
||||||
|
this@CollectionFolderViewModel.filter.value = filter
|
||||||
}
|
}
|
||||||
val includeItemTypes =
|
val includeItemTypes =
|
||||||
when (item.data.collectionType) {
|
when (item.data.collectionType) {
|
||||||
|
|
@ -112,6 +116,7 @@ class CollectionFolderViewModel
|
||||||
else -> listOf()
|
else -> listOf()
|
||||||
}
|
}
|
||||||
val request =
|
val request =
|
||||||
|
filter.applyTo(
|
||||||
GetItemsRequest(
|
GetItemsRequest(
|
||||||
parentId = item.id,
|
parentId = item.id,
|
||||||
enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB),
|
enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB),
|
||||||
|
|
@ -131,6 +136,7 @@ class CollectionFolderViewModel
|
||||||
SortOrder.ASCENDING,
|
SortOrder.ASCENDING,
|
||||||
),
|
),
|
||||||
fields = DefaultItemFields,
|
fields = DefaultItemFields,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
val newPager =
|
val newPager =
|
||||||
ApiRequestPager(
|
ApiRequestPager(
|
||||||
|
|
@ -182,7 +188,9 @@ class CollectionFolderViewModel
|
||||||
@Composable
|
@Composable
|
||||||
fun CollectionFolderGrid(
|
fun CollectionFolderGrid(
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
destination: Destination.MediaItem,
|
itemId: UUID,
|
||||||
|
item: BaseItem?,
|
||||||
|
initialFilter: GetItemsFilter,
|
||||||
recursive: Boolean,
|
recursive: Boolean,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (BaseItem) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -196,9 +204,10 @@ fun CollectionFolderGrid(
|
||||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
OneTimeLaunchedEffect {
|
OneTimeLaunchedEffect {
|
||||||
viewModel.init(destination.itemId, destination.item, initialSortAndDirection, recursive)
|
viewModel.init(itemId, item, initialSortAndDirection, recursive, initialFilter)
|
||||||
}
|
}
|
||||||
val sortAndDirection by viewModel.sortAndDirection.observeAsState(initialSortAndDirection)
|
val sortAndDirection by viewModel.sortAndDirection.observeAsState(initialSortAndDirection)
|
||||||
|
val filter by viewModel.filter.observeAsState(initialFilter)
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||||
val item by viewModel.item.observeAsState()
|
val item by viewModel.item.observeAsState()
|
||||||
val pager by viewModel.pager.observeAsState()
|
val pager by viewModel.pager.observeAsState()
|
||||||
|
|
@ -218,7 +227,7 @@ fun CollectionFolderGrid(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
onSortChange = {
|
onSortChange = {
|
||||||
viewModel.loadResults(it, recursive)
|
viewModel.loadResults(it, recursive, filter)
|
||||||
},
|
},
|
||||||
showTitle = showTitle,
|
showTitle = showTitle,
|
||||||
positionCallback = positionCallback,
|
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,
|
modifier: Modifier = Modifier,
|
||||||
initialPosition: Int = 0,
|
initialPosition: Int = 0,
|
||||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
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 startPosition = initialPosition.coerceIn(0, (pager.size - 1).coerceAtLeast(0))
|
||||||
val columns = 6
|
val columns = 6
|
||||||
|
|
@ -249,16 +262,15 @@ fun CardGrid(
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
val item = pager[index]
|
val item = pager[index]
|
||||||
GridCard(
|
cardContent(
|
||||||
item = item,
|
item,
|
||||||
onClick = {
|
{
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
focusedIndex = index
|
focusedIndex = index
|
||||||
onClickItem.invoke(item)
|
onClickItem.invoke(item)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongClick = { if (item != null) onLongClickItem.invoke(item) },
|
{ if (item != null) onLongClickItem.invoke(item) },
|
||||||
modifier =
|
|
||||||
mod
|
mod
|
||||||
.ifElse(index == 0, Modifier.focusRequester(zeroFocus))
|
.ifElse(index == 0, Modifier.focusRequester(zeroFocus))
|
||||||
.onFocusChanged { focusState ->
|
.onFocusChanged { focusState ->
|
||||||
|
|
|
||||||
|
|
@ -9,24 +9,30 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
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.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
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 com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CollectionFolderGeneric(
|
fun CollectionFolderGeneric(
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
destination: Destination.MediaItem,
|
itemId: UUID,
|
||||||
|
item: BaseItem?,
|
||||||
recursive: Boolean,
|
recursive: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
filter: GetItemsFilter = GetItemsFilter(),
|
||||||
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
|
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
var showHeader by remember { mutableStateOf(true) }
|
var showHeader by remember { mutableStateOf(true) }
|
||||||
CollectionFolderGrid(
|
CollectionFolderGrid(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
onClickItem = { preferencesViewModel.navigationManager.navigateTo(it.destination()) },
|
onClickItem = { preferencesViewModel.navigationManager.navigateTo(it.destination()) },
|
||||||
destination = destination,
|
itemId = itemId,
|
||||||
|
item = item,
|
||||||
|
initialFilter = filter,
|
||||||
showTitle = showHeader,
|
showTitle = showHeader,
|
||||||
recursive = recursive,
|
recursive = recursive,
|
||||||
modifier =
|
modifier =
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,12 @@ import androidx.tv.material3.TabRow
|
||||||
import androidx.tv.material3.TabRowDefaults
|
import androidx.tv.material3.TabRowDefaults
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
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.UserPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.rememberTab
|
import com.github.damontecres.wholphin.preferences.rememberTab
|
||||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
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.components.RecommendedMovie
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
|
@ -55,7 +57,7 @@ fun CollectionFolderMovie(
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
val tabs = listOf("Recommended", "Library")
|
val tabs = listOf("Recommended", "Library", "Genres")
|
||||||
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
@ -154,7 +156,9 @@ fun CollectionFolderMovie(
|
||||||
CollectionFolderGrid(
|
CollectionFolderGrid(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
destination = destination,
|
itemId = destination.itemId,
|
||||||
|
item = destination.item,
|
||||||
|
initialFilter = GetItemsFilter(),
|
||||||
showTitle = false,
|
showTitle = false,
|
||||||
recursive = true,
|
recursive = true,
|
||||||
modifier =
|
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)
|
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.TabRowDefaults
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
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.UserPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.rememberTab
|
import com.github.damontecres.wholphin.preferences.rememberTab
|
||||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
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.components.RecommendedTvShow
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
|
@ -55,7 +57,7 @@ fun CollectionFolderTv(
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
val tabs = listOf("Recommended", "Library")
|
val tabs = listOf("Recommended", "Library", "Genres")
|
||||||
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
@ -153,7 +155,9 @@ fun CollectionFolderTv(
|
||||||
1 -> {
|
1 -> {
|
||||||
CollectionFolderGrid(
|
CollectionFolderGrid(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
destination = destination,
|
itemId = destination.itemId,
|
||||||
|
item = destination.item,
|
||||||
|
initialFilter = GetItemsFilter(),
|
||||||
showTitle = false,
|
showTitle = false,
|
||||||
recursive = true,
|
recursive = true,
|
||||||
modifier =
|
modifier =
|
||||||
|
|
@ -167,6 +171,16 @@ fun CollectionFolderTv(
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
2 -> {
|
||||||
|
GenreCardGrid(
|
||||||
|
itemId = destination.itemId,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(start = 16.dp)
|
||||||
|
.fillMaxSize()
|
||||||
|
.focusRequester(focusRequester),
|
||||||
|
)
|
||||||
|
}
|
||||||
else -> ErrorMessage("Invalid tab index $selectedTabIndex", null)
|
else -> ErrorMessage("Invalid tab index $selectedTabIndex", null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ package com.github.damontecres.wholphin.ui.nav
|
||||||
|
|
||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
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.detail.series.SeasonEpisode
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
|
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
|
||||||
import com.github.damontecres.wholphin.util.UuidSerializer
|
import com.github.damontecres.wholphin.util.UuidSerializer
|
||||||
|
|
@ -75,6 +76,13 @@ sealed class Destination(
|
||||||
constructor(item: BaseItem) : this(item.id, item.resumeMs ?: 0, item)
|
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
|
@Serializable
|
||||||
data object UpdateApp : Destination(true)
|
data object UpdateApp : Destination(true)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,8 @@ fun DestinationContent(
|
||||||
BaseItemKind.BOX_SET ->
|
BaseItemKind.BOX_SET ->
|
||||||
CollectionFolderGeneric(
|
CollectionFolderGeneric(
|
||||||
preferences,
|
preferences,
|
||||||
destination,
|
destination.itemId,
|
||||||
|
destination.item,
|
||||||
false,
|
false,
|
||||||
modifier,
|
modifier,
|
||||||
)
|
)
|
||||||
|
|
@ -117,7 +118,8 @@ fun DestinationContent(
|
||||||
CollectionType.BOXSETS ->
|
CollectionType.BOXSETS ->
|
||||||
CollectionFolderGeneric(
|
CollectionFolderGeneric(
|
||||||
preferences,
|
preferences,
|
||||||
destination,
|
destination.itemId,
|
||||||
|
destination.item,
|
||||||
true,
|
true,
|
||||||
modifier,
|
modifier,
|
||||||
)
|
)
|
||||||
|
|
@ -125,7 +127,8 @@ fun DestinationContent(
|
||||||
else ->
|
else ->
|
||||||
CollectionFolderGeneric(
|
CollectionFolderGeneric(
|
||||||
preferences,
|
preferences,
|
||||||
destination,
|
destination.itemId,
|
||||||
|
destination.item,
|
||||||
false,
|
false,
|
||||||
modifier,
|
modifier,
|
||||||
)
|
)
|
||||||
|
|
@ -141,7 +144,8 @@ fun DestinationContent(
|
||||||
BaseItemKind.USER_VIEW ->
|
BaseItemKind.USER_VIEW ->
|
||||||
CollectionFolderGeneric(
|
CollectionFolderGeneric(
|
||||||
preferences,
|
preferences,
|
||||||
destination,
|
destination.itemId,
|
||||||
|
destination.item,
|
||||||
true,
|
true,
|
||||||
modifier,
|
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.UpdateApp -> InstallUpdatePage(preferences, modifier)
|
||||||
|
|
||||||
Destination.License -> LicenseInfo(modifier)
|
Destination.License -> LicenseInfo(modifier)
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,14 @@ import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.Response
|
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.itemsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.playlistsApi
|
import org.jellyfin.sdk.api.client.extensions.playlistsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.suggestionsApi
|
import org.jellyfin.sdk.api.client.extensions.suggestionsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult
|
import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult
|
||||||
import org.jellyfin.sdk.model.api.request.GetEpisodesRequest
|
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.GetItemsRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetNextUpRequest
|
import org.jellyfin.sdk.model.api.request.GetNextUpRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetPlaylistItemsRequest
|
import org.jellyfin.sdk.model.api.request.GetPlaylistItemsRequest
|
||||||
|
|
@ -292,3 +294,22 @@ val GetPlaylistItemsRequestHandler =
|
||||||
request: GetPlaylistItemsRequest,
|
request: GetPlaylistItemsRequest,
|
||||||
): Response<BaseItemDtoQueryResult> = api.playlistsApi.getPlaylistItems(request)
|
): 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