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 <damontecres@gmail.com>
This commit is contained in:
YogiBear12 2025-12-18 04:16:46 +11:00 committed by GitHub
parent 801ff5e67b
commit 2908f539e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -22,6 +22,7 @@ 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.layout.LazyLayoutCacheWindow import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -33,11 +34,14 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.FocusRequester
import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged 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.Key
import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key 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.text.style.TextAlign
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
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.LocalContentColor
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text 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.AppColors
import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.cards.GridCard 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.ifElse
import com.github.damontecres.wholphin.ui.playback.isBackwardButton import com.github.damontecres.wholphin.ui.playback.isBackwardButton
import com.github.damontecres.wholphin.ui.playback.isForwardButton import com.github.damontecres.wholphin.ui.playback.isForwardButton
@ -366,7 +370,11 @@ fun CardGrid(
AlphabetButtons( AlphabetButtons(
letters = letters, letters = letters,
currentLetter = currentLetter, 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 -> letterClicked = { letter ->
scope.launch(ExceptionHandler()) { scope.launch(ExceptionHandler()) {
val jumpPosition = val jumpPosition =
@ -375,6 +383,7 @@ fun CardGrid(
} }
Timber.d("Alphabet jump to $jumpPosition") Timber.d("Alphabet jump to $jumpPosition")
if (jumpPosition >= 0) { if (jumpPosition >= 0) {
pager.getOrNull(jumpPosition)
gridState.scrollToItem(jumpPosition) gridState.scrollToItem(jumpPosition)
focusOn(jumpPosition) focusOn(jumpPosition)
alphabetFocus = true alphabetFocus = true
@ -428,7 +437,6 @@ fun AlphabetButtons(
letterClicked: (Char) -> Unit, letterClicked: (Char) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val listState = rememberLazyListState() val listState = rememberLazyListState()
@ -440,17 +448,27 @@ fun AlphabetButtons(
listState.layoutInfo.visibleItemsInfo listState.layoutInfo.visibleItemsInfo
.lastOrNull() .lastOrNull()
?.index ?: -1 ?.index ?: -1
if (index < firstVisibleItemIndex || index > lastVisibleItemIndex) { if (index !in firstVisibleItemIndex..lastVisibleItemIndex) {
listState.animateScrollToItem(index) listState.animateScrollToItem(index)
} }
} }
} }
// Focus & interaction states for each letter button
val focusRequesters = remember { List(letters.length) { FocusRequester() } } 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( LazyColumn(
contentPadding = PaddingValues(4.dp), contentPadding = PaddingValues(vertical = 1.1.dp, horizontal = 2.dp),
verticalArrangement = Arrangement.spacedBy(1.1.dp),
state = listState, state = listState,
modifier = modifier =
modifier.focusProperties { modifier
.onFocusChanged { focusState ->
alphabetPickerFocused = focusState.hasFocus
}.focusProperties {
onEnter = { onEnter = {
focusRequesters[index.coerceIn(0, letters.length - 1)].tryRequestFocus() focusRequesters[index.coerceIn(0, letters.length - 1)].tryRequestFocus()
} }
@ -460,32 +478,68 @@ fun AlphabetButtons(
letters.length, letters.length,
key = { letters[it] }, key = { letters[it] },
) { index -> ) { index ->
val interactionSource = remember { MutableInteractionSource() } val interactionSource = interactionSources[index]
val focused by interactionSource.collectIsFocusedAsState() 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 =
Modifier Modifier
.size(24.dp) .size(14.dp)
.clip(CircleShape)
.alpha(itemAlpha),
) {
Button(
modifier =
Modifier
.size(14.dp)
.focusRequester(focusRequesters[index]), .focusRequester(focusRequesters[index]),
contentPadding = PaddingValues(2.dp), contentPadding = PaddingValues(0.dp), // No padding to maximize text space
interactionSource = interactionSource, interactionSource = interactionSource,
onClick = { onClick = {
letterClicked.invoke(letters[index]) letterClicked.invoke(letters[index])
}, },
) { colors =
val color = if (isCurrentLetter || focused) {
if (!focused && letters[index] == currentLetter) { // Use default button colors for current letter or focused
MaterialTheme.colorScheme.tertiary ButtonDefaults.colors()
} else { } else {
LocalContentColor.current // 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(
text = letters[index].toString(), text = letters[index].toString(),
color = color, color = color,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodySmall,
) )
} }
} }
} }
} }
}