Barebones sorting

This commit is contained in:
Damontecres 2025-10-05 18:17:29 -04:00
parent b0b791f325
commit 25859a05f7
No known key found for this signature in database
3 changed files with 253 additions and 91 deletions

View file

@ -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<ItemSortBy>,
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,
),
)
},
)
}
}
}
}

View file

@ -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,
)

View file

@ -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<Library>(api) {
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
val pager = MutableLiveData<ApiRequestPager<GetItemsRequest>?>()
val pager = MutableLiveData<List<BaseItem?>>(listOf())
val sortAndDirection = MutableLiveData<SortAndDirection>()
fun init(
itemId: UUID,
potential: BaseItem?,
sortAndDirection: SortAndDirection,
): Job =
viewModelScope.launch(
LoadingExceptionHandler(
@ -67,13 +74,17 @@ class CollectionFolderViewModel
) + Dispatchers.IO,
) {
fetchItem(itemId, potential)
setup()
loadResults(sortAndDirection)
}
private suspend fun setup() =
withContext(Dispatchers.IO) {
if (!pager.isInitialized) {
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()
@ -89,7 +100,7 @@ class CollectionFolderViewModel
CollectionType.LIVETV -> TODO()
CollectionType.PLAYLISTS -> TODO()
CollectionType.FOLDERS -> TODO()
null -> TODO()
null -> listOf()
}
val request =
GetItemsRequest(
@ -99,9 +110,9 @@ class CollectionFolderViewModel
// 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),
sortBy = listOf(sortAndDirection.sort),
sortOrder = listOf(sortAndDirection.direction),
fields = DefaultItemFields,
)
val newPager =
ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope)
@ -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(
CollectionDetails(
preferences,
navigationManager,
library!!,
item!!,
pager,
modifier,
sortAndDirection = sortAndDirection,
modifier = modifier,
onSortChange = {
viewModel.loadResults(it)
},
)
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()
}
}
}
}
}
@Composable
fun TVShowCollectionDetails(
fun CollectionDetails(
preferences: UserPreferences,
navigationManager: NavigationManager,
library: Library,
item: BaseItem,
pager: List<BaseItem?>,
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 = {