From 25859a05f78ed355834b89226f7ec6ab41aff501 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Sun, 5 Oct 2025 18:17:29 -0400 Subject: [PATCH] Barebones sorting --- .../dolphin/ui/components/SortByButton.kt | 117 ++++++++++++ .../dolphin/ui/data/SortAndDirection.kt | 47 +++++ .../ui/detail/CollectionFolderDetails.kt | 180 +++++++++--------- 3 files changed, 253 insertions(+), 91 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/dolphin/ui/components/SortByButton.kt create mode 100644 app/src/main/java/com/github/damontecres/dolphin/ui/data/SortAndDirection.kt diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/components/SortByButton.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/components/SortByButton.kt new file mode 100644 index 00000000..0c22c53a --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/components/SortByButton.kt @@ -0,0 +1,117 @@ +package com.github.damontecres.dolphin.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 +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +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.dolphin.R +import com.github.damontecres.dolphin.ui.FontAwesome +import com.github.damontecres.dolphin.ui.data.SortAndDirection +import com.github.damontecres.dolphin.ui.data.flip +import org.jellyfin.sdk.model.api.ItemSortBy +import org.jellyfin.sdk.model.api.SortOrder + +@Composable +fun SortByButton( + sortOptions: List, + current: SortAndDirection, + onSortChange: (SortAndDirection) -> Unit, + modifier: Modifier = Modifier, +) { + val currentSort = current.sort + val currentDirection = current.direction + var sortByDropDown by remember { mutableStateOf(false) } + val context = LocalContext.current + + Box(modifier = modifier) { + Button( + onClick = { sortByDropDown = true }, + onLongClick = { + onSortChange.invoke(current.flip()) + }, + ) { + Text( + text = + buildAnnotatedString { + withStyle(SpanStyle(fontFamily = FontAwesome)) { + append( + stringResource( + if (currentDirection == SortOrder.ASCENDING) { + R.string.fa_caret_up + } else { + R.string.fa_caret_down + }, + ), + ) + } + append(" ") + append(currentSort.name) // TODO names + }, + ) + } + + // TODO switch to dialog? + DropdownMenu( + expanded = sortByDropDown, + onDismissRequest = { sortByDropDown = false }, + ) { + sortOptions + .sortedBy { it.name } + .forEach { sortOption -> + DropdownMenuItem( + 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, + ) + } + } + }, + text = { + Text( + text = sortOption.name, + color = MaterialTheme.colorScheme.onSecondaryContainer, + ) + }, + onClick = { + sortByDropDown = false + val newDirection = + if (currentSort == sortOption) { + currentDirection.flip() + } else { + currentDirection + } + onSortChange.invoke( + SortAndDirection( + sortOption, + newDirection, + ), + ) + }, + ) + } + } + } +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/data/SortAndDirection.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/data/SortAndDirection.kt new file mode 100644 index 00000000..7770c340 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/data/SortAndDirection.kt @@ -0,0 +1,47 @@ +package com.github.damontecres.dolphin.ui.data + +import org.jellyfin.sdk.model.api.ItemSortBy +import org.jellyfin.sdk.model.api.SortOrder + +data class SortAndDirection( + val sort: ItemSortBy, + val direction: SortOrder, +) { + fun flip() = copy(direction = direction.flip()) +} + +fun SortOrder.flip() = if (this == SortOrder.ASCENDING) SortOrder.DESCENDING else SortOrder.ASCENDING + +val MovieSortOptions = + listOf( + ItemSortBy.SORT_NAME, + ItemSortBy.PRODUCTION_YEAR, + ItemSortBy.COMMUNITY_RATING, + ItemSortBy.DATE_CREATED, + ItemSortBy.DATE_PLAYED, + ItemSortBy.PLAY_COUNT, + ItemSortBy.STUDIO, + ItemSortBy.OFFICIAL_RATING, + ItemSortBy.RANDOM, + ) + +val SeriesSortOptions = + listOf( + ItemSortBy.SORT_NAME, + ItemSortBy.PREMIERE_DATE, + ItemSortBy.COMMUNITY_RATING, + ItemSortBy.DATE_CREATED, + ItemSortBy.DATE_LAST_CONTENT_ADDED, + ItemSortBy.DATE_PLAYED, + ItemSortBy.STUDIO, + ItemSortBy.OFFICIAL_RATING, + ItemSortBy.RANDOM, + ) + +val VideoSortOptions = + listOf( + ItemSortBy.SORT_NAME, + ItemSortBy.DATE_CREATED, + ItemSortBy.DATE_PLAYED, + ItemSortBy.RANDOM, + ) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt index 6adc5bd6..4cc6f50a 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt @@ -21,9 +21,15 @@ import androidx.tv.material3.Text import com.github.damontecres.dolphin.data.model.BaseItem import com.github.damontecres.dolphin.data.model.Library import com.github.damontecres.dolphin.preferences.UserPreferences +import com.github.damontecres.dolphin.ui.DefaultItemFields import com.github.damontecres.dolphin.ui.OneTimeLaunchedEffect import com.github.damontecres.dolphin.ui.components.ErrorMessage import com.github.damontecres.dolphin.ui.components.LoadingPage +import com.github.damontecres.dolphin.ui.components.SortByButton +import com.github.damontecres.dolphin.ui.data.MovieSortOptions +import com.github.damontecres.dolphin.ui.data.SeriesSortOptions +import com.github.damontecres.dolphin.ui.data.SortAndDirection +import com.github.damontecres.dolphin.ui.data.VideoSortOptions import com.github.damontecres.dolphin.ui.nav.Destination import com.github.damontecres.dolphin.ui.nav.NavigationManager import com.github.damontecres.dolphin.ui.tryRequestFocus @@ -40,7 +46,6 @@ import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.ImageType -import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.request.GetItemsRequest @@ -54,11 +59,13 @@ class CollectionFolderViewModel api: ApiClient, ) : ItemViewModel(api) { val loading = MutableLiveData(LoadingState.Loading) - val pager = MutableLiveData?>() + val pager = MutableLiveData>(listOf()) + val sortAndDirection = MutableLiveData() fun init( itemId: UUID, potential: BaseItem?, + sortAndDirection: SortAndDirection, ): Job = viewModelScope.launch( LoadingExceptionHandler( @@ -67,53 +74,57 @@ class CollectionFolderViewModel ) + Dispatchers.IO, ) { fetchItem(itemId, potential) - setup() + loadResults(sortAndDirection) } - private suspend fun setup() = - withContext(Dispatchers.IO) { - if (!pager.isInitialized) { - item.value?.let { item -> - val includeItemTypes = - when (item.data.collectionType) { - CollectionType.UNKNOWN -> TODO() - CollectionType.MOVIES -> listOf(BaseItemKind.MOVIE) - CollectionType.TVSHOWS -> listOf(BaseItemKind.SERIES) - CollectionType.MUSIC -> TODO() - CollectionType.MUSICVIDEOS -> TODO() - CollectionType.TRAILERS -> TODO() - CollectionType.HOMEVIDEOS -> listOf(BaseItemKind.VIDEO) - CollectionType.BOXSETS -> TODO() - CollectionType.BOOKS -> TODO() - CollectionType.PHOTOS -> TODO() - CollectionType.LIVETV -> TODO() - CollectionType.PLAYLISTS -> TODO() - CollectionType.FOLDERS -> TODO() - null -> TODO() - } - val request = - GetItemsRequest( - parentId = item.id, - isSeries = true, - mediaTypes = null, -// recursive = true, - enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB), - includeItemTypes = includeItemTypes, - sortBy = listOf(ItemSortBy.SORT_NAME), - sortOrder = listOf(SortOrder.ASCENDING), - fields = listOf(ItemFields.PRIMARY_IMAGE_ASPECT_RATIO), - ) - val newPager = - ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope) - newPager.init() - newPager.getBlocking(0) - withContext(Dispatchers.Main) { - pager.value = newPager - loading.value = LoadingState.Success + fun loadResults(sortAndDirection: SortAndDirection) { + item.value?.let { item -> + viewModelScope.launch(Dispatchers.IO) { + withContext(Dispatchers.Main) { + pager.value = listOf() + loading.value = LoadingState.Loading + this@CollectionFolderViewModel.sortAndDirection.value = sortAndDirection + } + val includeItemTypes = + when (item.data.collectionType) { + CollectionType.UNKNOWN -> TODO() + CollectionType.MOVIES -> listOf(BaseItemKind.MOVIE) + CollectionType.TVSHOWS -> listOf(BaseItemKind.SERIES) + CollectionType.MUSIC -> TODO() + CollectionType.MUSICVIDEOS -> TODO() + CollectionType.TRAILERS -> TODO() + CollectionType.HOMEVIDEOS -> listOf(BaseItemKind.VIDEO) + CollectionType.BOXSETS -> TODO() + CollectionType.BOOKS -> TODO() + CollectionType.PHOTOS -> TODO() + CollectionType.LIVETV -> TODO() + CollectionType.PLAYLISTS -> TODO() + CollectionType.FOLDERS -> TODO() + null -> listOf() } + val request = + GetItemsRequest( + parentId = item.id, + isSeries = true, + mediaTypes = null, +// recursive = true, + enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB), + includeItemTypes = includeItemTypes, + sortBy = listOf(sortAndDirection.sort), + sortOrder = listOf(sortAndDirection.direction), + fields = DefaultItemFields, + ) + val newPager = + ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope) + newPager.init() + newPager.getBlocking(0) + withContext(Dispatchers.Main) { + pager.value = newPager + loading.value = LoadingState.Success } } } + } } @Composable @@ -123,10 +134,16 @@ fun CollectionFolderDetails( destination: Destination.MediaItem, modifier: Modifier = Modifier, viewModel: CollectionFolderViewModel = hiltViewModel(), + initialSortAndDirection: SortAndDirection = + SortAndDirection( + ItemSortBy.SORT_NAME, + SortOrder.ASCENDING, + ), ) { OneTimeLaunchedEffect { - viewModel.init(destination.itemId, destination.item) + viewModel.init(destination.itemId, destination.item, initialSortAndDirection) } + val sortAndDirection by viewModel.sortAndDirection.observeAsState(initialSortAndDirection) val loading by viewModel.loading.observeAsState(LoadingState.Loading) val item by viewModel.item.observeAsState() val library by viewModel.model.observeAsState() @@ -137,67 +154,42 @@ fun CollectionFolderDetails( LoadingState.Loading -> LoadingPage() LoadingState.Success -> { pager?.let { pager -> - when (library!!.collectionType) { - CollectionType.UNKNOWN -> TODO() - - // TODO? - CollectionType.MOVIES -> - TVShowCollectionDetails( - preferences, - navigationManager, - library!!, - item!!, - pager, - modifier, - ) - - CollectionType.TVSHOWS -> { - TVShowCollectionDetails( - preferences, - navigationManager, - library!!, - item!!, - pager, - modifier, - ) - } - - // TODO? - CollectionType.HOMEVIDEOS -> - TVShowCollectionDetails( - preferences, - navigationManager, - library!!, - item!!, - pager, - modifier, - ) - - CollectionType.MUSIC -> TODO() - CollectionType.MUSICVIDEOS -> TODO() - CollectionType.TRAILERS -> TODO() - CollectionType.BOXSETS -> TODO() - CollectionType.BOOKS -> TODO() - CollectionType.PHOTOS -> TODO() - CollectionType.LIVETV -> TODO() - CollectionType.PLAYLISTS -> TODO() - CollectionType.FOLDERS -> TODO() - } + CollectionDetails( + preferences, + navigationManager, + library!!, + item!!, + pager, + sortAndDirection = sortAndDirection, + modifier = modifier, + onSortChange = { + viewModel.loadResults(it) + }, + ) } } } } @Composable -fun TVShowCollectionDetails( +fun CollectionDetails( preferences: UserPreferences, navigationManager: NavigationManager, library: Library, item: BaseItem, pager: List, + sortAndDirection: SortAndDirection, + onSortChange: (SortAndDirection) -> Unit, modifier: Modifier = Modifier, ) { val title = library.name ?: item.data.name ?: item.data.collectionType?.name ?: "Collection" + val sortOptions = + when (item.data.collectionType) { + CollectionType.MOVIES -> MovieSortOptions + CollectionType.TVSHOWS -> SeriesSortOptions + CollectionType.HOMEVIDEOS -> VideoSortOptions + else -> listOf(ItemSortBy.SORT_NAME, ItemSortBy.DATE_CREATED, ItemSortBy.RANDOM) + } val gridFocusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { gridFocusRequester.tryRequestFocus() } @@ -212,6 +204,12 @@ fun TVShowCollectionDetails( textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth(), ) + SortByButton( + sortOptions = sortOptions, + current = sortAndDirection, + onSortChange = onSortChange, + modifier = Modifier, + ) CardGrid( pager = pager, itemOnClick = {