diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/UiConstants.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/UiConstants.kt index 8c361c6c..42242e8a 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/UiConstants.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/UiConstants.kt @@ -27,6 +27,7 @@ val DefaultItemFields = ItemFields.CHILD_COUNT, ItemFields.OVERVIEW, ItemFields.TRICKPLAY, + ItemFields.SORT_NAME, ) @Preview( diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt index 449679d6..a2ad4d39 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt @@ -3,6 +3,8 @@ package com.github.damontecres.dolphin.ui.detail import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.focusGroup +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -10,11 +12,15 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -37,6 +43,7 @@ import androidx.compose.ui.input.key.type import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.tv.material3.Button +import androidx.tv.material3.LocalContentColor import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.github.damontecres.dolphin.R @@ -63,6 +70,7 @@ fun CardGrid( letterPosition: suspend (Char) -> Int, gridFocusRequester: FocusRequester, showJumpButtons: Boolean, + showLetterButtons: Boolean, modifier: Modifier = Modifier, initialPosition: Int = 0, positionCallback: ((columns: Int, position: Int) -> Unit)? = null, @@ -87,12 +95,13 @@ fun CardGrid( } // Wait for a recomposition to focus -// LaunchedEffect(alphabetFocus) { -// if (alphabetFocus) { -// firstFocus.tryRequestFocus() -// } -// alphabetFocus = false -// } + val alphabetFocusRequester = remember { FocusRequester() } + LaunchedEffect(alphabetFocus) { + if (alphabetFocus) { + alphabetFocusRequester.tryRequestFocus() + } + alphabetFocus = false + } val useBackToJump = true // uiConfig.preferences.interfacePreferences.scrollTopOnBack val showFooter = true // uiConfig.preferences.interfacePreferences.showPositionFooter @@ -215,7 +224,7 @@ fun CardGrid( .focusProperties { onExit = { // Leaving the grid, so "forget" the position - focusedIndex = -1 +// focusedIndex = -1 } onEnter = { if (focusedIndex < 0 && gridState.firstVisibleItemIndex <= startPosition) { @@ -231,6 +240,7 @@ fun CardGrid( Modifier .focusRequester(firstFocus) .focusRequester(gridFocusRequester) + .focusRequester(alphabetFocusRequester) } else { Modifier } @@ -299,18 +309,38 @@ fun CardGrid( } } // Letters - if (pager.isNotEmpty() && false) { - // TODO + val currentLetter = + remember(focusedIndex) { + pager + .getOrNull(focusedIndex) + ?.data + ?.sortName + ?.first() + ?.uppercaseChar() + ?.let { + if (it >= '0' && it <= '9') { + '#' + } else if (it >= 'A' && it <= 'Z') { + it + } else { + null + } + } + ?: LETTERS[0] + } + if (showLetterButtons && pager.isNotEmpty()) { AlphabetButtons( + currentLetter = currentLetter, modifier = Modifier.align(Alignment.CenterVertically), letterClicked = { letter -> scope.launch(ExceptionHandler()) { val jumpPosition = letterPosition.invoke(letter) Timber.d("Alphabet jump to $jumpPosition") - gridState.scrollToItem(jumpPosition) - focusOn(jumpPosition) - alphabetFocus = true -// firstFocus.tryRequestFocus() + if (jumpPosition >= 0) { + gridState.scrollToItem(jumpPosition) + focusOn(jumpPosition) + alphabetFocus = true + } } }, ) @@ -353,26 +383,68 @@ fun JumpButton( } } +private const val LETTERS = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ" + @Composable fun AlphabetButtons( + currentLetter: Char, letterClicked: (Char) -> Unit, modifier: Modifier = Modifier, ) { -// LazyColumn(modifier = modifier) { -// items( -// AlphabetSearchUtils.LETTERS.length, -// key = { AlphabetSearchUtils.LETTERS[it] }, -// ) { index -> -// Button( -// modifier = -// Modifier.size(24.dp), -// contentPadding = PaddingValues(2.dp), -// onClick = { -// letterClicked.invoke(AlphabetSearchUtils.LETTERS[index]) -// }, -// ) { -// Text(text = AlphabetSearchUtils.LETTERS[index].toString()) -// } -// } -// } + val scope = rememberCoroutineScope() + val listState = rememberLazyListState() + + val index = LETTERS.indexOf(currentLetter) + LaunchedEffect(currentLetter) { + scope.launch(ExceptionHandler()) { + val firstVisibleItemIndex = listState.firstVisibleItemIndex + val lastVisibleItemIndex = + listState.layoutInfo.visibleItemsInfo + .lastOrNull() + ?.index ?: -1 + if (index < firstVisibleItemIndex || index > lastVisibleItemIndex) { + listState.animateScrollToItem(index) + } + } + } + val focusRequesters = remember { List(LETTERS.length) { FocusRequester() } } + LazyColumn( + state = listState, + modifier = + modifier.focusProperties { + onEnter = { + focusRequesters[index.coerceIn(0, LETTERS.length - 1)].tryRequestFocus() + } + }, + ) { + items( + LETTERS.length, + key = { LETTERS[it] }, + ) { index -> + val interactionSource = remember { MutableInteractionSource() } + val focused by interactionSource.collectIsFocusedAsState() + Button( + modifier = + Modifier + .size(24.dp) + .focusRequester(focusRequesters[index]), + contentPadding = PaddingValues(2.dp), + interactionSource = interactionSource, + onClick = { + letterClicked.invoke(LETTERS[index]) + }, + ) { + val color = + if (!focused && LETTERS[index] == currentLetter) { + MaterialTheme.colorScheme.tertiary + } else { + LocalContentColor.current + } + Text( + text = LETTERS[index].toString(), + color = color, + ) + } + } + } } 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 ab272173..95951748 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 @@ -128,6 +128,28 @@ class CollectionFolderViewModel } } } + + suspend fun positionOfLetter(letter: Char): Int? = + item.value?.let { item -> + val includeItemTypes = + when (item.data.collectionType) { + CollectionType.MOVIES -> listOf(BaseItemKind.MOVIE) + CollectionType.TVSHOWS -> listOf(BaseItemKind.SERIES) + CollectionType.HOMEVIDEOS -> listOf(BaseItemKind.VIDEO) + + else -> listOf() + } + val request = + GetItemsRequest( + parentId = item.id, + includeItemTypes = includeItemTypes, + nameLessThan = letter.toString(), + limit = 0, + enableTotalRecordCount = true, + ) + val result by GetItemsRequestHandler.execute(api, request) + result.totalRecordCount + } } @Composable @@ -172,6 +194,7 @@ fun CollectionFolderDetails( }, showTitle = showTitle, positionCallback = positionCallback, + letterPosition = { viewModel.positionOfLetter(it) ?: -1 }, ) } } @@ -187,6 +210,7 @@ fun CollectionDetails( sortAndDirection: SortAndDirection, onClickItem: (BaseItem) -> Unit, onSortChange: (SortAndDirection) -> Unit, + letterPosition: suspend (Char) -> Int, modifier: Modifier = Modifier, showTitle: Boolean = true, positionCallback: ((columns: Int, position: Int) -> Unit)? = null, @@ -233,9 +257,10 @@ fun CollectionDetails( pager = pager, onClickItem = onClickItem, longClicker = {}, - letterPosition = { 0 }, + letterPosition = letterPosition, gridFocusRequester = gridFocusRequester, showJumpButtons = false, // TODO add preference + showLetterButtons = sortAndDirection.sort == ItemSortBy.SORT_NAME, modifier = Modifier.fillMaxSize(), initialPosition = 0, positionCallback = { columns, position ->