From 5a4528541abb4d7f38a4d4c4ad55826654e86fcf Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 2 Mar 2026 17:30:39 -0500 Subject: [PATCH] Better shuffle & repeat buttons --- .../ui/detail/music/NowPlayingButtons.kt | 219 ++++++++++++++++-- 1 file changed, 198 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingButtons.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingButtons.kt index 41880198..8726c786 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingButtons.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingButtons.kt @@ -1,14 +1,29 @@ package com.github.damontecres.wholphin.ui.detail.music import androidx.annotation.OptIn +import androidx.compose.foundation.background +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 +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.ui.compose.state.rememberNextButtonState @@ -16,13 +31,20 @@ import androidx.media3.ui.compose.state.rememberPlayPauseButtonState import androidx.media3.ui.compose.state.rememberPreviousButtonState import androidx.media3.ui.compose.state.rememberRepeatButtonState import androidx.media3.ui.compose.state.rememberShuffleButtonState +import androidx.tv.material3.ClickableSurfaceDefaults import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Text import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.ui.AppColors +import com.github.damontecres.wholphin.ui.FontAwesome +import com.github.damontecres.wholphin.ui.PreviewTvSpec +import com.github.damontecres.wholphin.ui.components.Button import com.github.damontecres.wholphin.ui.playback.ControllerViewState import com.github.damontecres.wholphin.ui.playback.PlaybackAction import com.github.damontecres.wholphin.ui.playback.PlaybackButtons -import com.github.damontecres.wholphin.ui.playback.PlaybackFaButton import com.github.damontecres.wholphin.ui.playback.buttonSpacing +import com.github.damontecres.wholphin.ui.theme.PreviewInteractionSource +import com.github.damontecres.wholphin.ui.theme.WholphinTheme import kotlin.time.Duration.Companion.seconds @OptIn(UnstableApi::class) @@ -74,35 +96,190 @@ fun NowPlayingButtons( horizontalArrangement = Arrangement.spacedBy(buttonSpacing), modifier = Modifier.align(Alignment.CenterEnd), ) { - PlaybackFaButton( - iconRes = R.string.fa_shuffle, + ShuffleButton( + active = shuffleState.shuffleOn, + enabled = shuffleState.isEnabled, onClick = { shuffleState.onClick() }, onControllerInteraction = { controllerViewState.pulseControls() }, - textColor = - if (shuffleState.shuffleOn) { - MaterialTheme.colorScheme.secondary - } else { - Color.Unspecified - }, ) - PlaybackFaButton( - iconRes = R.string.fa_repeat, + RepeatButton( + repeatMode = repeatState.repeatModeState, + enabled = repeatState.isEnabled, onClick = { repeatState.onClick() }, onControllerInteraction = { controllerViewState.pulseControls() }, - textColor = - when (repeatState.repeatModeState) { - Player.REPEAT_MODE_ALL -> MaterialTheme.colorScheme.secondary - - // TODO - Player.REPEAT_MODE_ONE -> MaterialTheme.colorScheme.tertiary - - else -> Color.Unspecified - }, ) } } } + +@Composable +fun ShuffleButton( + active: Boolean, + onClick: () -> Unit, + onControllerInteraction: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + val selectedColor = MaterialTheme.colorScheme.border + val focused by interactionSource.collectIsFocusedAsState() + Button( + enabled = enabled, + onClick = onClick, +// shape = ButtonDefaults.shape(CircleShape), + colors = + ClickableSurfaceDefaults.colors( + containerColor = AppColors.TransparentBlack25, + focusedContainerColor = selectedColor, + ), + contentPadding = PaddingValues(4.dp), + interactionSource = interactionSource, + modifier = + modifier + .size(36.dp, 36.dp) + .onFocusChanged { onControllerInteraction.invoke() }, + ) { + Text( + text = stringResource(R.string.fa_shuffle), + fontSize = 18.sp, + fontFamily = FontAwesome, + textAlign = TextAlign.Center, + color = + when { + focused && active -> MaterialTheme.colorScheme.onSurface + focused && !active -> MaterialTheme.colorScheme.surface + !focused && active -> MaterialTheme.colorScheme.onSurface + else -> MaterialTheme.colorScheme.onSurface.copy(alpha = .5f) + }, + modifier = + Modifier + .fillMaxWidth() + .align(Alignment.CenterVertically), + ) + } +} + +@Composable +fun RepeatButton( + repeatMode: Int, + onClick: () -> Unit, + onControllerInteraction: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + val selectedColor = MaterialTheme.colorScheme.border + val focused by interactionSource.collectIsFocusedAsState() + Button( + enabled = enabled, + onClick = onClick, +// shape = ButtonDefaults.shape(CircleShape), + colors = + ClickableSurfaceDefaults.colors( + containerColor = AppColors.TransparentBlack25, + focusedContainerColor = selectedColor, + ), + contentPadding = PaddingValues(4.dp), + interactionSource = interactionSource, + modifier = + modifier + .size(36.dp, 36.dp) + .onFocusChanged { onControllerInteraction.invoke() }, + ) { + Box( + Modifier + .fillMaxWidth() + .align(Alignment.CenterVertically), + ) { + Text( + text = stringResource(R.string.fa_repeat), + fontSize = 18.sp, + fontFamily = FontAwesome, + textAlign = TextAlign.Center, + color = + if (focused) { + when (repeatMode) { + Player.REPEAT_MODE_ALL -> MaterialTheme.colorScheme.onSurface + Player.REPEAT_MODE_ONE -> MaterialTheme.colorScheme.onSurface + else -> MaterialTheme.colorScheme.surface + } + } else { + when (repeatMode) { + Player.REPEAT_MODE_ALL -> MaterialTheme.colorScheme.onSurface + Player.REPEAT_MODE_ONE -> MaterialTheme.colorScheme.onSurface + else -> MaterialTheme.colorScheme.onSurface.copy(alpha = .5f) + } + }, + modifier = + Modifier + .fillMaxWidth() + .align(Alignment.Center), + ) + if (repeatMode == Player.REPEAT_MODE_ONE) { + Text( + text = "1", + fontSize = 10.sp, + color = MaterialTheme.colorScheme.surface, + modifier = + Modifier + .offset(x = 4.dp) + .background( + color = MaterialTheme.colorScheme.onSurface, + shape = RoundedCornerShape(1.dp), + ).align(Alignment.BottomStart), + ) + } + } + } +} + +@PreviewTvSpec +@Composable +fun ShuffleButtonPreview() { + val source = remember { PreviewInteractionSource() } + WholphinTheme { + Column { + Row { + RepeatButton( + repeatMode = Player.REPEAT_MODE_OFF, + onClick = {}, + onControllerInteraction = {}, + ) + RepeatButton( + repeatMode = Player.REPEAT_MODE_ALL, + onClick = {}, + onControllerInteraction = {}, + ) + RepeatButton( + repeatMode = Player.REPEAT_MODE_ONE, + onClick = {}, + onControllerInteraction = {}, + ) + } + Row { + RepeatButton( + repeatMode = Player.REPEAT_MODE_OFF, + onClick = {}, + onControllerInteraction = {}, + interactionSource = source, + ) + RepeatButton( + repeatMode = Player.REPEAT_MODE_ALL, + onClick = {}, + onControllerInteraction = {}, + interactionSource = source, + ) + RepeatButton( + repeatMode = Player.REPEAT_MODE_ONE, + onClick = {}, + onControllerInteraction = {}, + interactionSource = source, + ) + } + } + } +}