mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 16:11:20 +02:00
Add ability to filter libraries, collection, etc (#308)
Adds a button to filter contents next to the sort button on grid pages The filters are saved and restored you return to the page. It works with #278, playing the filtered items in the sorted order. The drop down menus for both sorting & filtering are updated to closer match the overall look. Filters for: - Played or unplayed - Favorite or not - Genres - Community (star) rating - Parental rating - Video type (HD vs SD, etc) - Release year and/or decade
This commit is contained in:
parent
50dc4b9a35
commit
78e7eb421f
16 changed files with 1325 additions and 75 deletions
|
|
@ -6,12 +6,16 @@ import androidx.compose.animation.fadeIn
|
|||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -40,6 +44,18 @@ import androidx.tv.material3.Text
|
|||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.filter.CommunityRatingFilter
|
||||
import com.github.damontecres.wholphin.data.filter.DecadeFilter
|
||||
import com.github.damontecres.wholphin.data.filter.DefaultFilterOptions
|
||||
import com.github.damontecres.wholphin.data.filter.FavoriteFilter
|
||||
import com.github.damontecres.wholphin.data.filter.FilterValueOption
|
||||
import com.github.damontecres.wholphin.data.filter.FilterVideoType
|
||||
import com.github.damontecres.wholphin.data.filter.GenreFilter
|
||||
import com.github.damontecres.wholphin.data.filter.ItemFilterBy
|
||||
import com.github.damontecres.wholphin.data.filter.OfficialRatingFilter
|
||||
import com.github.damontecres.wholphin.data.filter.PlayedFilter
|
||||
import com.github.damontecres.wholphin.data.filter.VideoTypeFilter
|
||||
import com.github.damontecres.wholphin.data.filter.YearFilter
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride
|
||||
|
|
@ -72,9 +88,13 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.genresApi
|
||||
import org.jellyfin.sdk.api.client.extensions.localizationApi
|
||||
import org.jellyfin.sdk.api.client.extensions.yearsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
|
@ -84,6 +104,8 @@ 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 timber.log.Timber
|
||||
import java.util.TreeSet
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration
|
||||
|
|
@ -100,6 +122,7 @@ class CollectionFolderViewModel
|
|||
val navigationManager: NavigationManager,
|
||||
) : ItemViewModel(api) {
|
||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||
val backgroundLoading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||
val pager = MutableLiveData<List<BaseItem?>>(listOf())
|
||||
val sortAndDirection = MutableLiveData<SortAndDirection>()
|
||||
val filter = MutableLiveData<GetItemsFilter>(GetItemsFilter())
|
||||
|
|
@ -121,26 +144,62 @@ class CollectionFolderViewModel
|
|||
) {
|
||||
this@CollectionFolderViewModel.useSeriesForPrimary = useSeriesForPrimary
|
||||
this@CollectionFolderViewModel.itemId = itemId
|
||||
itemId?.toUUIDOrNull()?.let {
|
||||
itemId.toUUIDOrNull()?.let {
|
||||
fetchItem(it)
|
||||
}
|
||||
|
||||
val libraryDisplayInfo =
|
||||
serverRepository.currentUser.value?.let { user ->
|
||||
libraryDisplayInfoDao.getItem(user, itemId)
|
||||
}
|
||||
|
||||
val sortAndDirection =
|
||||
initialSortAndDirection
|
||||
?: (
|
||||
serverRepository.currentUser.value?.let { user ->
|
||||
libraryDisplayInfoDao.getItem(user, itemId)?.sortAndDirection
|
||||
} ?: SortAndDirection.DEFAULT
|
||||
)
|
||||
?: libraryDisplayInfo?.sortAndDirection
|
||||
?: SortAndDirection.DEFAULT
|
||||
|
||||
loadResults(sortAndDirection, recursive, filter, useSeriesForPrimary)
|
||||
val filterToUse =
|
||||
if (libraryDisplayInfo?.filter != null) {
|
||||
filter.merge(libraryDisplayInfo.filter)
|
||||
} else {
|
||||
filter
|
||||
}
|
||||
|
||||
loadResults(true, sortAndDirection, recursive, filterToUse, useSeriesForPrimary)
|
||||
}
|
||||
|
||||
fun onFilterChange(
|
||||
newFilter: GetItemsFilter,
|
||||
recursive: Boolean,
|
||||
) {
|
||||
Timber.v("onFilterChange: filter=%s", newFilter)
|
||||
serverRepository.currentUser.value?.let { user ->
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val libraryDisplayInfo =
|
||||
LibraryDisplayInfo(
|
||||
userId = user.rowId,
|
||||
itemId = itemId,
|
||||
sort = sortAndDirection.value!!.sort,
|
||||
direction = sortAndDirection.value!!.direction,
|
||||
filter = newFilter,
|
||||
)
|
||||
libraryDisplayInfoDao.saveItem(libraryDisplayInfo)
|
||||
}
|
||||
}
|
||||
loadResults(false, sortAndDirection.value!!, recursive, newFilter, useSeriesForPrimary)
|
||||
}
|
||||
|
||||
fun onSortChange(
|
||||
sortAndDirection: SortAndDirection,
|
||||
recursive: Boolean,
|
||||
filter: GetItemsFilter,
|
||||
) {
|
||||
Timber.v(
|
||||
"onSortChange: sort=%s, recursive=%s, filter=%s",
|
||||
sortAndDirection,
|
||||
recursive,
|
||||
filter,
|
||||
)
|
||||
serverRepository.currentUser.value?.let { user ->
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val libraryDisplayInfo =
|
||||
|
|
@ -149,14 +208,16 @@ class CollectionFolderViewModel
|
|||
itemId = itemId,
|
||||
sort = sortAndDirection.sort,
|
||||
direction = sortAndDirection.direction,
|
||||
filter = filter,
|
||||
)
|
||||
libraryDisplayInfoDao.saveItem(libraryDisplayInfo)
|
||||
}
|
||||
}
|
||||
loadResults(sortAndDirection, recursive, filter, useSeriesForPrimary)
|
||||
loadResults(true, sortAndDirection, recursive, filter, useSeriesForPrimary)
|
||||
}
|
||||
|
||||
private fun loadResults(
|
||||
resetState: Boolean,
|
||||
sortAndDirection: SortAndDirection,
|
||||
recursive: Boolean,
|
||||
filter: GetItemsFilter,
|
||||
|
|
@ -164,17 +225,22 @@ class CollectionFolderViewModel
|
|||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.Main) {
|
||||
pager.value = listOf()
|
||||
loading.value = LoadingState.Loading
|
||||
if (resetState) {
|
||||
pager.value = listOf()
|
||||
loading.value = LoadingState.Loading
|
||||
}
|
||||
backgroundLoading.value = LoadingState.Loading
|
||||
this@CollectionFolderViewModel.sortAndDirection.value = sortAndDirection
|
||||
this@CollectionFolderViewModel.filter.value = filter
|
||||
}
|
||||
delay(1000)
|
||||
val newPager = createPager(sortAndDirection, recursive, filter, useSeriesForPrimary)
|
||||
newPager.init()
|
||||
if (newPager.isNotEmpty()) newPager.getBlocking(0)
|
||||
withContext(Dispatchers.Main) {
|
||||
pager.value = newPager
|
||||
loading.value = LoadingState.Success
|
||||
backgroundLoading.value = LoadingState.Success
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -256,6 +322,78 @@ class CollectionFolderViewModel
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun getFilterOptionValues(filterOption: ItemFilterBy<*>): List<FilterValueOption> =
|
||||
try {
|
||||
when (filterOption) {
|
||||
GenreFilter -> {
|
||||
api.genresApi
|
||||
.getGenres(
|
||||
parentId = itemUuid,
|
||||
userId = serverRepository.currentUser.value?.id,
|
||||
).content.items
|
||||
.map { FilterValueOption(it.name ?: "", it.id) }
|
||||
}
|
||||
|
||||
FavoriteFilter,
|
||||
PlayedFilter,
|
||||
->
|
||||
listOf(
|
||||
FilterValueOption("True", null),
|
||||
FilterValueOption("False", null),
|
||||
)
|
||||
|
||||
OfficialRatingFilter -> {
|
||||
api.localizationApi.getParentalRatings().content.map {
|
||||
FilterValueOption(it.name ?: "", it.value)
|
||||
}
|
||||
}
|
||||
|
||||
VideoTypeFilter ->
|
||||
FilterVideoType.entries.map {
|
||||
FilterValueOption(it.readable, it)
|
||||
}
|
||||
|
||||
YearFilter -> {
|
||||
api.yearsApi
|
||||
.getYears(
|
||||
parentId = itemUuid,
|
||||
userId = serverRepository.currentUser.value?.id,
|
||||
sortBy = listOf(ItemSortBy.SORT_NAME),
|
||||
sortOrder = listOf(SortOrder.ASCENDING),
|
||||
).content.items
|
||||
.mapNotNull {
|
||||
it.name?.toIntOrNull()?.let { FilterValueOption(it.toString(), it) }
|
||||
}
|
||||
}
|
||||
|
||||
DecadeFilter -> {
|
||||
val items = TreeSet<Int>()
|
||||
api.yearsApi
|
||||
.getYears(
|
||||
parentId = itemUuid,
|
||||
userId = serverRepository.currentUser.value?.id,
|
||||
sortBy = listOf(ItemSortBy.SORT_NAME),
|
||||
sortOrder = listOf(SortOrder.ASCENDING),
|
||||
).content.items
|
||||
.mapNotNullTo(items) {
|
||||
it.name
|
||||
?.toIntOrNull()
|
||||
?.div(10)
|
||||
?.times(10)
|
||||
}
|
||||
items.toList().sorted().map { FilterValueOption("$it's", it) }
|
||||
}
|
||||
|
||||
CommunityRatingFilter ->
|
||||
(1..10).map {
|
||||
FilterValueOption("$it", it)
|
||||
}
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Exception get filter value options for $filterOption")
|
||||
listOf()
|
||||
}
|
||||
|
||||
suspend fun positionOfLetter(letter: Char): Int? =
|
||||
withContext(Dispatchers.IO) {
|
||||
item.value?.let { item ->
|
||||
|
|
@ -319,6 +457,8 @@ fun CollectionFolderGrid(
|
|||
showTitle: Boolean = true,
|
||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||
params: CollectionFolderGridParameters = CollectionFolderGridParameters(),
|
||||
useSeriesForPrimary: Boolean = true,
|
||||
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
|
||||
) = CollectionFolderGrid(
|
||||
preferences,
|
||||
itemId.toServerString(),
|
||||
|
|
@ -332,6 +472,8 @@ fun CollectionFolderGrid(
|
|||
showTitle = showTitle,
|
||||
positionCallback = positionCallback,
|
||||
params = params,
|
||||
useSeriesForPrimary = useSeriesForPrimary,
|
||||
filterOptions = filterOptions,
|
||||
)
|
||||
|
||||
@Composable
|
||||
|
|
@ -351,6 +493,7 @@ fun CollectionFolderGrid(
|
|||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||
params: CollectionFolderGridParameters = CollectionFolderGridParameters(),
|
||||
useSeriesForPrimary: Boolean = true,
|
||||
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
OneTimeLaunchedEffect {
|
||||
|
|
@ -362,9 +505,10 @@ fun CollectionFolderGrid(
|
|||
useSeriesForPrimary,
|
||||
)
|
||||
}
|
||||
val sortAndDirection by viewModel.sortAndDirection.observeAsState()
|
||||
val sortAndDirection by viewModel.sortAndDirection.observeAsState(SortAndDirection.DEFAULT)
|
||||
val filter by viewModel.filter.observeAsState(initialFilter)
|
||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||
val backgroundLoading by viewModel.backgroundLoading.observeAsState(LoadingState.Loading)
|
||||
val item by viewModel.item.observeAsState()
|
||||
val pager by viewModel.pager.observeAsState()
|
||||
|
||||
|
|
@ -379,40 +523,67 @@ fun CollectionFolderGrid(
|
|||
-> LoadingPage()
|
||||
LoadingState.Success -> {
|
||||
pager?.let { pager ->
|
||||
CollectionFolderGridContent(
|
||||
preferences,
|
||||
item,
|
||||
pager,
|
||||
sortAndDirection = sortAndDirection!!,
|
||||
modifier = modifier,
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = { position, item ->
|
||||
moreDialog.makePresent(PositionItem(position, item))
|
||||
},
|
||||
onSortChange = {
|
||||
viewModel.onSortChange(it, recursive, filter)
|
||||
},
|
||||
showTitle = showTitle,
|
||||
sortOptions = sortOptions,
|
||||
positionCallback = positionCallback,
|
||||
letterPosition = { viewModel.positionOfLetter(it) ?: -1 },
|
||||
params = params,
|
||||
playEnabled = playEnabled,
|
||||
onClickPlay = { shuffle ->
|
||||
itemId.toUUIDOrNull()?.let {
|
||||
viewModel.navigationManager.navigateTo(
|
||||
Destination.PlaybackList(
|
||||
itemId = it,
|
||||
startIndex = 0,
|
||||
shuffle = shuffle,
|
||||
recursive = recursive,
|
||||
sortAndDirection = sortAndDirection,
|
||||
filter = filter,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
Box(modifier = modifier) {
|
||||
CollectionFolderGridContent(
|
||||
preferences,
|
||||
item,
|
||||
pager,
|
||||
sortAndDirection = sortAndDirection!!,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = { position, item ->
|
||||
moreDialog.makePresent(PositionItem(position, item))
|
||||
},
|
||||
onSortChange = {
|
||||
viewModel.onSortChange(it, recursive, filter)
|
||||
},
|
||||
filterOptions = filterOptions,
|
||||
currentFilter = filter,
|
||||
onFilterChange = {
|
||||
viewModel.onFilterChange(it, recursive)
|
||||
},
|
||||
getPossibleFilterValues = {
|
||||
viewModel.getFilterOptionValues(it)
|
||||
},
|
||||
showTitle = showTitle,
|
||||
sortOptions = sortOptions,
|
||||
positionCallback = positionCallback,
|
||||
letterPosition = { viewModel.positionOfLetter(it) ?: -1 },
|
||||
params = params,
|
||||
playEnabled = playEnabled,
|
||||
onClickPlay = { shuffle ->
|
||||
itemId.toUUIDOrNull()?.let {
|
||||
viewModel.navigationManager.navigateTo(
|
||||
Destination.PlaybackList(
|
||||
itemId = it,
|
||||
startIndex = 0,
|
||||
shuffle = shuffle,
|
||||
recursive = recursive,
|
||||
sortAndDirection = sortAndDirection,
|
||||
filter = filter,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
backgroundLoading == LoadingState.Loading,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
CircularProgress(
|
||||
Modifier
|
||||
.background(
|
||||
MaterialTheme.colorScheme.background.copy(alpha = .25f),
|
||||
shape = CircleShape,
|
||||
).size(64.dp)
|
||||
.padding(4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -484,6 +655,10 @@ fun CollectionFolderGridContent(
|
|||
showTitle: Boolean = true,
|
||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||
params: CollectionFolderGridParameters = CollectionFolderGridParameters(),
|
||||
currentFilter: GetItemsFilter = GetItemsFilter(),
|
||||
filterOptions: List<ItemFilterBy<*>> = listOf(),
|
||||
onFilterChange: (GetItemsFilter) -> Unit = {},
|
||||
getPossibleFilterValues: suspend (ItemFilterBy<*>) -> List<FilterValueOption>,
|
||||
) {
|
||||
val title = item?.name ?: item?.data?.collectionType?.name ?: stringResource(R.string.collection)
|
||||
|
||||
|
|
@ -525,13 +700,30 @@ fun CollectionFolderGridContent(
|
|||
.padding(start = 16.dp, end = endPadding)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
if (sortOptions.isNotEmpty()) {
|
||||
SortByButton(
|
||||
sortOptions = sortOptions,
|
||||
current = sortAndDirection,
|
||||
onSortChange = onSortChange,
|
||||
if (sortOptions.isNotEmpty() || filterOptions.isNotEmpty()) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier,
|
||||
)
|
||||
) {
|
||||
if (sortOptions.isNotEmpty()) {
|
||||
SortByButton(
|
||||
sortOptions = sortOptions,
|
||||
current = sortAndDirection,
|
||||
onSortChange = onSortChange,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
if (filterOptions.isNotEmpty()) {
|
||||
FilterByButton(
|
||||
filterOptions = filterOptions,
|
||||
current = currentFilter,
|
||||
onFilterChange = onFilterChange,
|
||||
getPossibleValues = getPossibleFilterValues,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (playEnabled) {
|
||||
Row(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,384 @@
|
|||
package com.github.damontecres.wholphin.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.Button
|
||||
import androidx.tv.material3.Icon
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import androidx.tv.material3.surfaceColorAtElevation
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.filter.CommunityRatingFilter
|
||||
import com.github.damontecres.wholphin.data.filter.DecadeFilter
|
||||
import com.github.damontecres.wholphin.data.filter.FavoriteFilter
|
||||
import com.github.damontecres.wholphin.data.filter.FilterValueOption
|
||||
import com.github.damontecres.wholphin.data.filter.FilterVideoType
|
||||
import com.github.damontecres.wholphin.data.filter.GenreFilter
|
||||
import com.github.damontecres.wholphin.data.filter.ItemFilterBy
|
||||
import com.github.damontecres.wholphin.data.filter.OfficialRatingFilter
|
||||
import com.github.damontecres.wholphin.data.filter.PlayedFilter
|
||||
import com.github.damontecres.wholphin.data.filter.VideoTypeFilter
|
||||
import com.github.damontecres.wholphin.data.filter.YearFilter
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.ui.DefaultButtonPadding
|
||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
fun FilterByButton(
|
||||
filterOptions: List<ItemFilterBy<*>>,
|
||||
current: GetItemsFilter,
|
||||
onFilterChange: (GetItemsFilter) -> Unit,
|
||||
getPossibleValues: suspend (ItemFilterBy<*>) -> List<FilterValueOption>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var dropDown by remember { mutableStateOf(false) }
|
||||
var nestedDropDown by remember { mutableStateOf<ItemFilterBy<*>?>(null) }
|
||||
val filterCount = remember(current, filterOptions) { current.countFilters(filterOptions) }
|
||||
|
||||
Box(modifier = modifier) {
|
||||
ExpandableFilterButton(
|
||||
filterCount = filterCount,
|
||||
onClick = { dropDown = true },
|
||||
modifier = Modifier,
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
expanded = dropDown,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||
onDismissRequest = {
|
||||
dropDown = false
|
||||
nestedDropDown = null
|
||||
},
|
||||
) {
|
||||
filterOptions
|
||||
.forEach { filterOption ->
|
||||
val currentValue = remember(current) { filterOption.get(current) }
|
||||
TvDropdownMenuItem(
|
||||
leadingIcon = {
|
||||
if (currentValue != null) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Filter active",
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(filterOption.stringRes),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
nestedDropDown = filterOption
|
||||
},
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
if (filterCount > 0) {
|
||||
HorizontalDivider()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
TvDropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
tint = Color.Red.copy(alpha = .8f),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.remove),
|
||||
color = if (focused) MaterialTheme.colorScheme.onError else MaterialTheme.colorScheme.error,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onFilterChange.invoke(current.delete(filterOptions))
|
||||
dropDown = false
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = nestedDropDown != null,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp),
|
||||
offset = DpOffset(80.dp, 16.dp),
|
||||
onDismissRequest = {
|
||||
nestedDropDown = null
|
||||
},
|
||||
) {
|
||||
nestedDropDown?.let { filterOption ->
|
||||
filterOption as ItemFilterBy<Any>
|
||||
val currentValue = remember(current) { filterOption.get(current) }
|
||||
|
||||
var possibleValues by remember { mutableStateOf<List<FilterValueOption>?>(null) }
|
||||
LaunchedEffect(Unit) {
|
||||
possibleValues = getPossibleValues.invoke(filterOption)
|
||||
}
|
||||
|
||||
if (currentValue != null) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
TvDropdownMenuItem(
|
||||
elevation = 5.dp,
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
tint = Color.Red.copy(alpha = .8f),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.remove),
|
||||
color = if (focused) MaterialTheme.colorScheme.onError else MaterialTheme.colorScheme.error,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onFilterChange.invoke(filterOption.set(null, current))
|
||||
nestedDropDown = null
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
modifier = Modifier,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
if (possibleValues == null) {
|
||||
CircularProgress(Modifier.size(48.dp))
|
||||
} else if (possibleValues?.isEmpty() == true) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_results),
|
||||
)
|
||||
} else {
|
||||
possibleValues.orEmpty().forEachIndexed { index, value ->
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
if (index == 0) {
|
||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||
}
|
||||
|
||||
val isSelected =
|
||||
remember(currentValue) {
|
||||
when (filterOption) {
|
||||
GenreFilter -> {
|
||||
(currentValue as? List<UUID>)
|
||||
.orEmpty()
|
||||
.contains(value.value)
|
||||
}
|
||||
|
||||
FavoriteFilter,
|
||||
PlayedFilter,
|
||||
-> (currentValue as? Boolean) == value.name.toBoolean()
|
||||
|
||||
OfficialRatingFilter -> {
|
||||
(currentValue as? List<String>)
|
||||
.orEmpty()
|
||||
.contains(value.name)
|
||||
}
|
||||
|
||||
VideoTypeFilter ->
|
||||
(currentValue as? List<FilterVideoType>)
|
||||
.orEmpty()
|
||||
.contains(value.value)
|
||||
|
||||
YearFilter,
|
||||
DecadeFilter,
|
||||
->
|
||||
(currentValue as? List<Int>)
|
||||
.orEmpty()
|
||||
.contains(value.value)
|
||||
|
||||
CommunityRatingFilter -> (currentValue as? Int) == value.value
|
||||
}
|
||||
}
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
TvDropdownMenuItem(
|
||||
elevation = 8.dp,
|
||||
interactionSource = interactionSource,
|
||||
leadingIcon = {
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Filter active",
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
if (filterOption == CommunityRatingFilter) {
|
||||
SimpleStarRating(
|
||||
"${value.name}+",
|
||||
starColor = if (focused) EmptyStarColor else FilledStarColor,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = value.name,
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
val newFilter =
|
||||
when (filterOption) {
|
||||
GenreFilter -> {
|
||||
val list = (currentValue as? List<UUID>).orEmpty()
|
||||
val newValue =
|
||||
list
|
||||
.toMutableList()
|
||||
.apply {
|
||||
if (isSelected) {
|
||||
remove(value.value!!)
|
||||
} else {
|
||||
add(value.value!! as UUID)
|
||||
}
|
||||
}.takeIf { it.isNotEmpty() }
|
||||
filterOption.set(newValue, current)
|
||||
}
|
||||
|
||||
FavoriteFilter,
|
||||
PlayedFilter,
|
||||
-> {
|
||||
val played = value.name.toBoolean()
|
||||
filterOption.set(played, current)
|
||||
}
|
||||
|
||||
OfficialRatingFilter -> {
|
||||
val list = (currentValue as? List<String>).orEmpty()
|
||||
val newValue =
|
||||
list
|
||||
.toMutableList()
|
||||
.apply {
|
||||
if (isSelected) {
|
||||
remove(value.name)
|
||||
} else {
|
||||
add(value.name)
|
||||
}
|
||||
}.takeIf { it.isNotEmpty() }
|
||||
filterOption.set(newValue, current)
|
||||
}
|
||||
|
||||
VideoTypeFilter -> {
|
||||
val list =
|
||||
(currentValue as? List<FilterVideoType>).orEmpty()
|
||||
val newValue =
|
||||
list
|
||||
.toMutableList()
|
||||
.apply {
|
||||
if (isSelected) {
|
||||
remove(value.value)
|
||||
} else {
|
||||
add(value.value as FilterVideoType)
|
||||
}
|
||||
}.takeIf { it.isNotEmpty() }
|
||||
filterOption.set(newValue, current)
|
||||
}
|
||||
|
||||
YearFilter,
|
||||
DecadeFilter,
|
||||
-> {
|
||||
val list =
|
||||
(currentValue as? List<Int>).orEmpty()
|
||||
val newValue =
|
||||
list
|
||||
.toMutableList()
|
||||
.apply {
|
||||
if (isSelected) {
|
||||
remove(value.value)
|
||||
} else {
|
||||
add(value.value as Int)
|
||||
}
|
||||
}.takeIf { it.isNotEmpty() }
|
||||
filterOption.set(newValue, current)
|
||||
}
|
||||
|
||||
CommunityRatingFilter ->
|
||||
filterOption.set(
|
||||
value.value as? Int,
|
||||
current,
|
||||
)
|
||||
}
|
||||
|
||||
onFilterChange.invoke(newFilter)
|
||||
if (!filterOption.supportMultiple) {
|
||||
nestedDropDown = null
|
||||
}
|
||||
},
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExpandableFilterButton(
|
||||
filterCount: Int,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
iconColor: Color = Color.Unspecified,
|
||||
) {
|
||||
val resources = LocalResources.current
|
||||
val isFocused = interactionSource.collectIsFocusedAsState().value
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = modifier.heightIn(min = 40.dp),
|
||||
contentPadding = DefaultButtonPadding,
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.fa_filter),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = iconColor,
|
||||
fontSize = 16.sp,
|
||||
fontFamily = FontAwesome,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier,
|
||||
)
|
||||
AnimatedVisibility(filterCount > 0) {
|
||||
Text(
|
||||
text = filterCount.toString(),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(isFocused) {
|
||||
Text(
|
||||
text = stringResource(R.string.filter),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -38,10 +38,12 @@ import androidx.compose.ui.unit.Dp
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.Icon
|
||||
import androidx.tv.material3.LocalContentColor
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.ui.AppColors
|
||||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.playOnClickSound
|
||||
import com.github.damontecres.wholphin.ui.playSoundOnFocus
|
||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||
|
|
@ -67,7 +69,7 @@ enum class StarRatingPrecision {
|
|||
}
|
||||
|
||||
val FilledStarColor = Color(0xFFFFC700)
|
||||
val EmptyStarColor = Color(0x2AFFC700)
|
||||
val EmptyStarColor = Color(0xFF5D4A0A)
|
||||
|
||||
val ratingBarHeight: Dp = 32.dp
|
||||
|
||||
|
|
@ -75,22 +77,34 @@ val ratingBarHeight: Dp = 32.dp
|
|||
fun SimpleStarRating(
|
||||
communityRating: Float?,
|
||||
modifier: Modifier = Modifier,
|
||||
) = SimpleStarRating(
|
||||
text = communityRating?.let { String.format(Locale.getDefault(), "%.1f", it) },
|
||||
modifier = modifier,
|
||||
textColor = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun SimpleStarRating(
|
||||
text: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
textColor: Color = LocalContentColor.current,
|
||||
starColor: Color = FilledStarColor,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier,
|
||||
) {
|
||||
communityRating?.let {
|
||||
if (text.isNotNullOrBlank()) {
|
||||
Text(
|
||||
text = String.format(Locale.getDefault(), "%.1f", it),
|
||||
text = text,
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
color = textColor,
|
||||
modifier = Modifier,
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Star,
|
||||
tint = FilledStarColor,
|
||||
tint = starColor,
|
||||
contentDescription = null,
|
||||
modifier = Modifier,
|
||||
)
|
||||
|
|
@ -254,7 +268,7 @@ private fun SimpleStarRatingPreview() {
|
|||
Column {
|
||||
SimpleStarRating(7.5f, Modifier.height(32.dp))
|
||||
SimpleStarRating(7.5f, Modifier.height(20.dp))
|
||||
SimpleStarRating(null, Modifier.height(32.dp))
|
||||
SimpleStarRating("", Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.github.damontecres.wholphin.ui.components
|
|||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -15,7 +14,6 @@ import androidx.compose.ui.text.SpanStyle
|
|||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.tv.material3.Button
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||
|
|
@ -77,19 +75,17 @@ fun SortByButton(
|
|||
sortOptions
|
||||
// .sortedBy { it.name }
|
||||
.forEach { sortOption ->
|
||||
DropdownMenuItem(
|
||||
TvDropdownMenuItem(
|
||||
leadingIcon = {
|
||||
if (sortOption == currentSort) {
|
||||
if (currentDirection == SortOrder.ASCENDING) {
|
||||
Text(
|
||||
text = stringResource(R.string.fa_caret_up),
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
fontFamily = FontAwesome,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(R.string.fa_caret_down),
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
fontFamily = FontAwesome,
|
||||
)
|
||||
}
|
||||
|
|
@ -98,7 +94,6 @@ fun SortByButton(
|
|||
text = {
|
||||
Text(
|
||||
text = stringResource(getStringRes(sortOption)),
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
package com.github.damontecres.wholphin.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.MenuDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.LocalContentColor
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.surfaceColorAtElevation
|
||||
|
||||
@Composable
|
||||
fun TvDropdownMenuItem(
|
||||
text: @Composable () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
enabled: Boolean = true,
|
||||
contentPadding: PaddingValues = MenuDefaults.DropdownMenuItemContentPadding,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
elevation: Dp = 3.dp,
|
||||
) {
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
val backgroundColor =
|
||||
if (focused) {
|
||||
MaterialTheme.colorScheme.inverseSurface
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
|
||||
}
|
||||
val contentColor =
|
||||
if (focused) MaterialTheme.colorScheme.inverseOnSurface else MaterialTheme.colorScheme.onSurface
|
||||
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
||||
DropdownMenuItem(
|
||||
enabled = enabled,
|
||||
colors =
|
||||
MenuDefaults.itemColors(
|
||||
textColor = contentColor,
|
||||
leadingIconColor = contentColor,
|
||||
trailingIconColor = contentColor,
|
||||
),
|
||||
leadingIcon = leadingIcon,
|
||||
text = text,
|
||||
trailingIcon = trailingIcon,
|
||||
onClick = onClick,
|
||||
interactionSource = interactionSource,
|
||||
contentPadding = contentPadding,
|
||||
modifier =
|
||||
modifier.background(backgroundColor),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@ 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.filter.DefaultFilterOptions
|
||||
import com.github.damontecres.wholphin.data.filter.ItemFilterBy
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||
|
|
@ -26,6 +28,7 @@ fun CollectionFolderGeneric(
|
|||
playEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
filter: GetItemsFilter = GetItemsFilter(),
|
||||
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
|
||||
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
|
||||
) {
|
||||
var showHeader by remember { mutableStateOf(true) }
|
||||
|
|
@ -53,5 +56,6 @@ fun CollectionFolderGeneric(
|
|||
},
|
||||
params = params,
|
||||
playEnabled = playEnabled,
|
||||
filterOptions = filterOptions,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.filter.DefaultForFavoritesFilterOptions
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride
|
||||
|
|
@ -129,6 +130,7 @@ fun FavoritesPage(
|
|||
showHeader = position < columns
|
||||
},
|
||||
playEnabled = false,
|
||||
filterOptions = DefaultForFavoritesFilterOptions,
|
||||
)
|
||||
}
|
||||
// TV
|
||||
|
|
@ -154,6 +156,7 @@ fun FavoritesPage(
|
|||
showHeader = position < columns
|
||||
},
|
||||
playEnabled = false,
|
||||
filterOptions = DefaultForFavoritesFilterOptions,
|
||||
)
|
||||
}
|
||||
// Episodes
|
||||
|
|
@ -181,6 +184,7 @@ fun FavoritesPage(
|
|||
showHeader = position < columns
|
||||
},
|
||||
playEnabled = false,
|
||||
filterOptions = DefaultForFavoritesFilterOptions,
|
||||
)
|
||||
}
|
||||
// Videos
|
||||
|
|
@ -207,6 +211,7 @@ fun FavoritesPage(
|
|||
showHeader = position < columns
|
||||
},
|
||||
playEnabled = false,
|
||||
filterOptions = DefaultForFavoritesFilterOptions,
|
||||
)
|
||||
}
|
||||
// Playlists
|
||||
|
|
@ -233,6 +238,7 @@ fun FavoritesPage(
|
|||
showHeader = position < columns
|
||||
},
|
||||
playEnabled = false,
|
||||
filterOptions = DefaultForFavoritesFilterOptions,
|
||||
)
|
||||
}
|
||||
// People
|
||||
|
|
@ -263,6 +269,7 @@ fun FavoritesPage(
|
|||
showHeader = position < columns
|
||||
},
|
||||
playEnabled = false,
|
||||
filterOptions = listOf(),
|
||||
)
|
||||
}
|
||||
else -> ErrorMessage("Invalid tab index $selectedTabIndex", null)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.github.damontecres.wholphin.ui.nav
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.data.filter.DefaultForGenresFilterOptions
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.ItemGrid
|
||||
import com.github.damontecres.wholphin.ui.components.LicenseInfo
|
||||
|
|
@ -166,6 +167,7 @@ fun DestinationContent(
|
|||
recursive = destination.recursive,
|
||||
usePosters = true,
|
||||
playEnabled = true, // TODO only genres use this currently, so might need to change in future
|
||||
filterOptions = DefaultForGenresFilterOptions,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue