From 2908f539e2eafff70ebfbfbfda219894b01d4f5d Mon Sep 17 00:00:00 2001 From: YogiBear12 <139140546+YogiBear12@users.noreply.github.com> Date: Thu, 18 Dec 2025 04:16:46 +1100 Subject: [PATCH] Redesign alphabet picker to fit all letters on screen (#480) - Switch to tv.material3.Button with ButtonDefaults for better styling - Add opacity changes based on focus (0.85f focused, 0.2f unfocused) - Keep selected letter fully visible when picker is unfocused - Reduce button size from 24.dp to 14.dp to fit all letters - Reduce spacing between letters (1.1.dp vertical, 2.dp horizontal) - Use transparent backgrounds for unfocused letters - Use border color for selected letter when focused, tertiary when unfocused - Add Box with CircleShape clip to prevent focus indicator overflow - Reduce font size by 15% - Add 16.dp end padding to push picker away from screen edge When using the 'Show details' view option, the alphabet picker will still overflow the bottom edge of the screen. I find this an acceptable option though, as the backdrop pushes the card grid further down the page. Closes https://github.com/damontecres/Wholphin/issues/386 >[!NOTE] > AI was used in the making of this PR --------- Co-authored-by: Damontecres --- .../wholphin/ui/detail/CardGrid.kt | 118 +++++++++++++----- 1 file changed, 86 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt index 8ffba496..eeb16031 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -33,11 +34,14 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key @@ -49,6 +53,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.tv.material3.Button +import androidx.tv.material3.ButtonDefaults import androidx.tv.material3.LocalContentColor import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text @@ -57,8 +63,6 @@ import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.cards.GridCard -import com.github.damontecres.wholphin.ui.components.Button -import com.github.damontecres.wholphin.ui.components.TextButton import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.playback.isBackwardButton import com.github.damontecres.wholphin.ui.playback.isForwardButton @@ -366,7 +370,11 @@ fun CardGrid( AlphabetButtons( letters = letters, currentLetter = currentLetter, - modifier = Modifier.align(Alignment.CenterVertically), + modifier = + Modifier + .align(Alignment.CenterVertically) + .padding(end = 16.dp), + // Add end padding to push away from edge letterClicked = { letter -> scope.launch(ExceptionHandler()) { val jumpPosition = @@ -375,6 +383,7 @@ fun CardGrid( } Timber.d("Alphabet jump to $jumpPosition") if (jumpPosition >= 0) { + pager.getOrNull(jumpPosition) gridState.scrollToItem(jumpPosition) focusOn(jumpPosition) alphabetFocus = true @@ -428,7 +437,6 @@ fun AlphabetButtons( letterClicked: (Char) -> Unit, modifier: Modifier = Modifier, ) { - val context = LocalContext.current val scope = rememberCoroutineScope() val listState = rememberLazyListState() @@ -440,51 +448,97 @@ fun AlphabetButtons( listState.layoutInfo.visibleItemsInfo .lastOrNull() ?.index ?: -1 - if (index < firstVisibleItemIndex || index > lastVisibleItemIndex) { + if (index !in firstVisibleItemIndex..lastVisibleItemIndex) { listState.animateScrollToItem(index) } } } + // Focus & interaction states for each letter button val focusRequesters = remember { List(letters.length) { FocusRequester() } } + val interactionSources = remember { List(letters.length) { MutableInteractionSource() } } + + // Track if the entire alphabet picker component has focus + var alphabetPickerFocused by remember { mutableStateOf(false) } + LazyColumn( - contentPadding = PaddingValues(4.dp), + contentPadding = PaddingValues(vertical = 1.1.dp, horizontal = 2.dp), + verticalArrangement = Arrangement.spacedBy(1.1.dp), state = listState, modifier = - modifier.focusProperties { - onEnter = { - focusRequesters[index.coerceIn(0, letters.length - 1)].tryRequestFocus() - } - }, + modifier + .onFocusChanged { focusState -> + alphabetPickerFocused = focusState.hasFocus + }.focusProperties { + onEnter = { + focusRequesters[index.coerceIn(0, letters.length - 1)].tryRequestFocus() + } + }, ) { items( letters.length, key = { letters[it] }, ) { index -> - val interactionSource = remember { MutableInteractionSource() } + val interactionSource = interactionSources[index] val focused by interactionSource.collectIsFocusedAsState() - TextButton( + + val isCurrentLetter = letters[index] == currentLetter + // Apply alpha to individual items, but keep selected letter fully visible when picker is unfocused + val itemAlpha = + when { + isCurrentLetter && !alphabetPickerFocused -> 1f + alphabetPickerFocused -> .85f + else -> .25f + } + + // Only show circle background for the current letter (or when focused) + // Wrap in Box with clipping to prevent focus indicator from overflowing + Box( modifier = Modifier - .size(24.dp) - .focusRequester(focusRequesters[index]), - contentPadding = PaddingValues(2.dp), - interactionSource = interactionSource, - onClick = { - letterClicked.invoke(letters[index]) - }, + .size(14.dp) + .clip(CircleShape) + .alpha(itemAlpha), ) { - val color = - if (!focused && letters[index] == currentLetter) { - MaterialTheme.colorScheme.tertiary - } else { - LocalContentColor.current - } - Text( - text = letters[index].toString(), - color = color, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), - ) + Button( + modifier = + Modifier + .size(14.dp) + .focusRequester(focusRequesters[index]), + contentPadding = PaddingValues(0.dp), // No padding to maximize text space + interactionSource = interactionSource, + onClick = { + letterClicked.invoke(letters[index]) + }, + colors = + if (isCurrentLetter || focused) { + // Use default button colors for current letter or focused + ButtonDefaults.colors() + } else { + // Transparent background for non-current letters (no circle) + ButtonDefaults.colors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface, + focusedContainerColor = MaterialTheme.colorScheme.primaryContainer, + focusedContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ) + }, + ) { + // Use border color for selected letter when focused, tertiary for unfocused-selected + val color = + when { + isCurrentLetter && focused -> MaterialTheme.colorScheme.border + isCurrentLetter -> MaterialTheme.colorScheme.tertiary + focused -> LocalContentColor.current + else -> MaterialTheme.colorScheme.onSurface + } + Text( + text = letters[index].toString(), + color = color, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + style = MaterialTheme.typography.bodySmall, + ) + } } } }