Add support for letter jump buttons

This commit is contained in:
Damontecres 2025-10-08 21:37:36 -04:00
parent 560e7cc545
commit fe1d63793d
No known key found for this signature in database
3 changed files with 129 additions and 31 deletions

View file

@ -27,6 +27,7 @@ val DefaultItemFields =
ItemFields.CHILD_COUNT, ItemFields.CHILD_COUNT,
ItemFields.OVERVIEW, ItemFields.OVERVIEW,
ItemFields.TRICKPLAY, ItemFields.TRICKPLAY,
ItemFields.SORT_NAME,
) )
@Preview( @Preview(

View file

@ -3,6 +3,8 @@ package com.github.damontecres.dolphin.ui.detail
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.focusGroup 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.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width 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.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.Button import androidx.tv.material3.Button
import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.dolphin.R import com.github.damontecres.dolphin.R
@ -63,6 +70,7 @@ fun CardGrid(
letterPosition: suspend (Char) -> Int, letterPosition: suspend (Char) -> Int,
gridFocusRequester: FocusRequester, gridFocusRequester: FocusRequester,
showJumpButtons: Boolean, showJumpButtons: Boolean,
showLetterButtons: Boolean,
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,
@ -87,12 +95,13 @@ fun CardGrid(
} }
// Wait for a recomposition to focus // Wait for a recomposition to focus
// LaunchedEffect(alphabetFocus) { val alphabetFocusRequester = remember { FocusRequester() }
// if (alphabetFocus) { LaunchedEffect(alphabetFocus) {
// firstFocus.tryRequestFocus() if (alphabetFocus) {
// } alphabetFocusRequester.tryRequestFocus()
// alphabetFocus = false }
// } alphabetFocus = false
}
val useBackToJump = true // uiConfig.preferences.interfacePreferences.scrollTopOnBack val useBackToJump = true // uiConfig.preferences.interfacePreferences.scrollTopOnBack
val showFooter = true // uiConfig.preferences.interfacePreferences.showPositionFooter val showFooter = true // uiConfig.preferences.interfacePreferences.showPositionFooter
@ -215,7 +224,7 @@ fun CardGrid(
.focusProperties { .focusProperties {
onExit = { onExit = {
// Leaving the grid, so "forget" the position // Leaving the grid, so "forget" the position
focusedIndex = -1 // focusedIndex = -1
} }
onEnter = { onEnter = {
if (focusedIndex < 0 && gridState.firstVisibleItemIndex <= startPosition) { if (focusedIndex < 0 && gridState.firstVisibleItemIndex <= startPosition) {
@ -231,6 +240,7 @@ fun CardGrid(
Modifier Modifier
.focusRequester(firstFocus) .focusRequester(firstFocus)
.focusRequester(gridFocusRequester) .focusRequester(gridFocusRequester)
.focusRequester(alphabetFocusRequester)
} else { } else {
Modifier Modifier
} }
@ -299,18 +309,38 @@ fun CardGrid(
} }
} }
// Letters // Letters
if (pager.isNotEmpty() && false) { val currentLetter =
// TODO 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( AlphabetButtons(
currentLetter = currentLetter,
modifier = Modifier.align(Alignment.CenterVertically), modifier = Modifier.align(Alignment.CenterVertically),
letterClicked = { letter -> letterClicked = { letter ->
scope.launch(ExceptionHandler()) { scope.launch(ExceptionHandler()) {
val jumpPosition = letterPosition.invoke(letter) val jumpPosition = letterPosition.invoke(letter)
Timber.d("Alphabet jump to $jumpPosition") Timber.d("Alphabet jump to $jumpPosition")
gridState.scrollToItem(jumpPosition) if (jumpPosition >= 0) {
focusOn(jumpPosition) gridState.scrollToItem(jumpPosition)
alphabetFocus = true focusOn(jumpPosition)
// firstFocus.tryRequestFocus() alphabetFocus = true
}
} }
}, },
) )
@ -353,26 +383,68 @@ fun JumpButton(
} }
} }
private const val LETTERS = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ"
@Composable @Composable
fun AlphabetButtons( fun AlphabetButtons(
currentLetter: Char,
letterClicked: (Char) -> Unit, letterClicked: (Char) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
// LazyColumn(modifier = modifier) { val scope = rememberCoroutineScope()
// items( val listState = rememberLazyListState()
// AlphabetSearchUtils.LETTERS.length,
// key = { AlphabetSearchUtils.LETTERS[it] }, val index = LETTERS.indexOf(currentLetter)
// ) { index -> LaunchedEffect(currentLetter) {
// Button( scope.launch(ExceptionHandler()) {
// modifier = val firstVisibleItemIndex = listState.firstVisibleItemIndex
// Modifier.size(24.dp), val lastVisibleItemIndex =
// contentPadding = PaddingValues(2.dp), listState.layoutInfo.visibleItemsInfo
// onClick = { .lastOrNull()
// letterClicked.invoke(AlphabetSearchUtils.LETTERS[index]) ?.index ?: -1
// }, if (index < firstVisibleItemIndex || index > lastVisibleItemIndex) {
// ) { listState.animateScrollToItem(index)
// Text(text = AlphabetSearchUtils.LETTERS[index].toString()) }
// } }
// } }
// } 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,
)
}
}
}
} }

View file

@ -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 @Composable
@ -172,6 +194,7 @@ fun CollectionFolderDetails(
}, },
showTitle = showTitle, showTitle = showTitle,
positionCallback = positionCallback, positionCallback = positionCallback,
letterPosition = { viewModel.positionOfLetter(it) ?: -1 },
) )
} }
} }
@ -187,6 +210,7 @@ fun CollectionDetails(
sortAndDirection: SortAndDirection, sortAndDirection: SortAndDirection,
onClickItem: (BaseItem) -> Unit, onClickItem: (BaseItem) -> Unit,
onSortChange: (SortAndDirection) -> Unit, onSortChange: (SortAndDirection) -> Unit,
letterPosition: suspend (Char) -> Int,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showTitle: Boolean = true, showTitle: Boolean = true,
positionCallback: ((columns: Int, position: Int) -> Unit)? = null, positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
@ -233,9 +257,10 @@ fun CollectionDetails(
pager = pager, pager = pager,
onClickItem = onClickItem, onClickItem = onClickItem,
longClicker = {}, longClicker = {},
letterPosition = { 0 }, letterPosition = letterPosition,
gridFocusRequester = gridFocusRequester, gridFocusRequester = gridFocusRequester,
showJumpButtons = false, // TODO add preference showJumpButtons = false, // TODO add preference
showLetterButtons = sortAndDirection.sort == ItemSortBy.SORT_NAME,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
initialPosition = 0, initialPosition = 0,
positionCallback = { columns, position -> positionCallback = { columns, position ->