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.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,17 +448,27 @@ 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 {
modifier
.onFocusChanged { focusState ->
alphabetPickerFocused = focusState.hasFocus
}.focusProperties {
onEnter = {
focusRequesters[index.coerceIn(0, letters.length - 1)].tryRequestFocus()
}
@ -460,32 +478,68 @@ fun AlphabetButtons(
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)
.size(14.dp)
.clip(CircleShape)
.alpha(itemAlpha),
) {
Button(
modifier =
Modifier
.size(14.dp)
.focusRequester(focusRequesters[index]),
contentPadding = PaddingValues(2.dp),
contentPadding = PaddingValues(0.dp), // No padding to maximize text space
interactionSource = interactionSource,
onClick = {
letterClicked.invoke(letters[index])
},
) {
val color =
if (!focused && letters[index] == currentLetter) {
MaterialTheme.colorScheme.tertiary
colors =
if (isCurrentLetter || focused) {
// Use default button colors for current letter or focused
ButtonDefaults.colors()
} 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 = letters[index].toString(),
color = color,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodySmall,
)
}
}
}
}
}