mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
chore: Android TV UI clean-up
This commit is contained in:
parent
fedc00c65b
commit
721fc28060
9 changed files with 119 additions and 74 deletions
|
|
@ -1,16 +1,16 @@
|
|||
package nl.jknaapen.fladder.composables.controls
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -23,42 +23,37 @@ import androidx.compose.ui.focus.onFocusChanged
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import nl.jknaapen.fladder.utility.conditional
|
||||
import nl.jknaapen.fladder.utility.highlightOnFocus
|
||||
|
||||
@Composable
|
||||
internal fun CustomIconButton(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit,
|
||||
enabled: Boolean = true,
|
||||
enableFocusIndicator: Boolean = true,
|
||||
enableScaledFocus: Boolean = false,
|
||||
backgroundColor: Color = Color.Transparent,
|
||||
backgroundColor: Color = Color.White.copy(alpha = 0.1f),
|
||||
foreGroundColor: Color = Color.White,
|
||||
backgroundFocusedColor: Color = Color.Transparent,
|
||||
foreGroundFocusedColor: Color = Color.White,
|
||||
backgroundFocusedColor: Color = Color.White,
|
||||
foreGroundFocusedColor: Color = Color.Black,
|
||||
icon: @Composable () -> Unit,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val currentContentColor by remember {
|
||||
derivedStateOf {
|
||||
|
||||
val currentContentColor by animateColorAsState(
|
||||
if (isFocused) {
|
||||
foreGroundFocusedColor
|
||||
} else {
|
||||
foreGroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}, label = "buttonContentColor"
|
||||
)
|
||||
|
||||
val currentBackgroundColor by remember {
|
||||
derivedStateOf {
|
||||
val currentBackgroundColor by animateColorAsState(
|
||||
if (isFocused) {
|
||||
backgroundFocusedColor
|
||||
} else {
|
||||
backgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}, label = "buttonBackground"
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
|
|
@ -66,10 +61,7 @@ internal fun CustomIconButton(
|
|||
.conditional(enableScaledFocus) {
|
||||
scale(if (isFocused) 1.05f else 1f)
|
||||
}
|
||||
.conditional(enableFocusIndicator) {
|
||||
highlightOnFocus()
|
||||
}
|
||||
.background(currentBackgroundColor, shape = RoundedCornerShape(8.dp))
|
||||
.background(currentBackgroundColor, shape = CircleShape)
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.clickable(
|
||||
enabled = enabled,
|
||||
|
|
@ -81,7 +73,7 @@ internal fun CustomIconButton(
|
|||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CompositionLocalProvider(LocalContentColor provides currentContentColor) {
|
||||
Box(modifier = Modifier.padding(8.dp)) {
|
||||
Box(modifier = Modifier.padding(16.dp)) {
|
||||
icon()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import MediaSegment
|
|||
import MediaSegmentType
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
|
|
@ -57,6 +59,7 @@ import androidx.compose.ui.input.key.type
|
|||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastCoerceIn
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
|
|
@ -95,7 +98,9 @@ internal fun ProgressBar(
|
|||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp, alignment = Alignment.CenterVertically)
|
||||
) {
|
||||
val playbackData by VideoPlayerObject.implementation.playbackData.collectAsState(null)
|
||||
if (scrubbingTimeLine)
|
||||
FilmstripTrickPlayOverlay(
|
||||
|
|
@ -108,20 +113,23 @@ internal fun ProgressBar(
|
|||
trickPlayModel = playbackData?.trickPlayModel
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
val subTitle = playableData?.subTitle
|
||||
subTitle?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyLarge.copy(color = Color.White),
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
)
|
||||
}
|
||||
VideoEndTime()
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
8.dp,
|
||||
16.dp,
|
||||
alignment = Alignment.CenterHorizontally
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
|
@ -130,7 +138,9 @@ internal fun ProgressBar(
|
|||
Text(
|
||||
formatTime(currentPosition),
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
)
|
||||
SimpleProgressBar(
|
||||
player,
|
||||
|
|
@ -153,7 +163,9 @@ internal fun ProgressBar(
|
|||
)
|
||||
),
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -200,7 +212,7 @@ internal fun RowScope.SimpleProgressBar(
|
|||
width = it.size.width
|
||||
}
|
||||
)
|
||||
.heightIn(min = 26.dp)
|
||||
.heightIn(min = 42.dp)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures { offset ->
|
||||
onUserInteraction()
|
||||
|
|
@ -241,14 +253,19 @@ internal fun RowScope.SimpleProgressBar(
|
|||
modifier = Modifier
|
||||
.focusable(enabled = false)
|
||||
.fillMaxWidth()
|
||||
.height(8.dp)
|
||||
.height(10.dp)
|
||||
.background(
|
||||
color = Color.Black.copy(
|
||||
color = Color.White.copy(
|
||||
alpha = 0.15f
|
||||
),
|
||||
shape = slideBarShape
|
||||
),
|
||||
) {
|
||||
|
||||
val animatedBarColor by animateColorAsState(
|
||||
if (thumbFocused) MaterialTheme.colorScheme.primary else Color.White,
|
||||
label = "progressBarColor"
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.focusable(enabled = false)
|
||||
|
|
@ -256,9 +273,7 @@ internal fun RowScope.SimpleProgressBar(
|
|||
.fillMaxWidth(progress)
|
||||
.padding(end = 8.dp)
|
||||
.background(
|
||||
color = if (thumbFocused) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.primary.copy(
|
||||
alpha = 0.75f
|
||||
),
|
||||
color = animatedBarColor,
|
||||
shape = slideBarShape
|
||||
)
|
||||
)
|
||||
|
|
@ -292,10 +307,10 @@ internal fun RowScope.SimpleProgressBar(
|
|||
.focusable(enabled = false)
|
||||
.graphicsLayer {
|
||||
translationX = startPx
|
||||
translationY = 16.dp.toPx()
|
||||
translationY = 20.dp.toPx()
|
||||
}
|
||||
.width(segDp)
|
||||
.height(6.dp)
|
||||
.height(8.dp)
|
||||
.background(
|
||||
color = segment.color.copy(alpha = 0.75f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
|
|
@ -318,6 +333,7 @@ internal fun RowScope.SimpleProgressBar(
|
|||
val durMs = duration.toDouble().coerceAtLeast(1.0)
|
||||
val startPx = (width * (segStartMs / durMs)).toFloat()
|
||||
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterStart)
|
||||
|
|
@ -331,7 +347,7 @@ internal fun RowScope.SimpleProgressBar(
|
|||
.aspectRatio(ratio = 1f)
|
||||
.background(
|
||||
color = if (isAfterCurrentPositon) Color.White.copy(
|
||||
alpha = 0.5f
|
||||
alpha = 0.15f
|
||||
) else MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f),
|
||||
shape = CircleShape
|
||||
)
|
||||
|
|
@ -339,6 +355,12 @@ internal fun RowScope.SimpleProgressBar(
|
|||
}
|
||||
}
|
||||
|
||||
val animatedThumbHeight by animateDpAsState(
|
||||
if (thumbFocused) 28.dp else 14.dp,
|
||||
label = "Thumb height"
|
||||
)
|
||||
|
||||
|
||||
//Thumb
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
|
@ -414,8 +436,8 @@ internal fun RowScope.SimpleProgressBar(
|
|||
color = Color.White,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.width(8.dp)
|
||||
.height(if (thumbFocused) 21.dp else 8.dp)
|
||||
.width(14.dp)
|
||||
.height(animatedThumbHeight)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||
internal fun BoxScope.SegmentSkipOverlay(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
||||
val isAndroidTV = leanBackEnabled(LocalContext.current)
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.compose.runtime.collectAsState
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import nl.jknaapen.fladder.objects.VideoPlayerObject
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
|
@ -39,6 +40,9 @@ fun VideoEndTime() {
|
|||
|
||||
Text(
|
||||
text = "ends at $formattedEnd",
|
||||
style = MaterialTheme.typography.bodyLarge.copy(color = Color.White),
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import androidx.compose.ui.focus.onFocusChanged
|
|||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.input.key.KeyEvent
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
|
|
@ -96,6 +97,7 @@ fun CustomVideoControls(
|
|||
|
||||
val buffering by VideoPlayerObject.buffering.collectAsState(true)
|
||||
val playing by VideoPlayerObject.playing.collectAsState(false)
|
||||
val controlsPadding = 32.dp
|
||||
|
||||
ImmersiveSystemBars(isImmersive = !showControls)
|
||||
|
||||
|
|
@ -131,10 +133,13 @@ fun CustomVideoControls(
|
|||
if (keyEvent.type != KeyEventType.KeyDown) return@onKeyEvent false
|
||||
if (!showControls) {
|
||||
bottomControlFocusRequester.requestFocus()
|
||||
}
|
||||
updateLastInteraction()
|
||||
return@onKeyEvent true
|
||||
} else {
|
||||
updateLastInteraction()
|
||||
return@onKeyEvent false
|
||||
}
|
||||
}
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = interactionSource,
|
||||
|
|
@ -170,7 +175,7 @@ fun CustomVideoControls(
|
|||
),
|
||||
)
|
||||
.safeContentPadding(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp, alignment = Alignment.Start)
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp, alignment = Alignment.Start)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
|
|
@ -203,7 +208,7 @@ fun CustomVideoControls(
|
|||
// Progress Bar
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(horizontal = controlsPadding)
|
||||
.displayCutoutPadding(),
|
||||
) {
|
||||
ProgressBar(
|
||||
|
|
@ -227,8 +232,8 @@ fun CustomVideoControls(
|
|||
),
|
||||
)
|
||||
.displayCutoutPadding()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 8.dp, bottom = 16.dp)
|
||||
.padding(horizontal = controlsPadding)
|
||||
.padding(top = 8.dp, bottom = controlsPadding)
|
||||
) {
|
||||
LeftButtons(
|
||||
openChapterSelection = {
|
||||
|
|
@ -245,6 +250,9 @@ fun CustomVideoControls(
|
|||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.align(alignment = Alignment.Center)
|
||||
.size(64.dp),
|
||||
strokeCap = StrokeCap.Round,
|
||||
strokeWidth = 12.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -302,7 +310,7 @@ fun PlaybackButtons(
|
|||
.padding(horizontal = 4.dp, vertical = 6.dp)
|
||||
.wrapContentWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
8.dp,
|
||||
16.dp,
|
||||
alignment = Alignment.CenterHorizontally
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
|
@ -315,7 +323,6 @@ fun PlaybackButtons(
|
|||
Iconsax.Filled.Backward,
|
||||
modifier = Modifier.size(32.dp),
|
||||
contentDescription = previousVideo,
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
CustomIconButton(
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ internal fun ChapterSelectionSheet(
|
|||
val chapters = playbackData?.chapters ?: listOf()
|
||||
val currentPosition by VideoPlayerObject.position.collectAsState(0L)
|
||||
|
||||
if (chapters.isEmpty()) return
|
||||
|
||||
var currentChapter: Chapter? by remember {
|
||||
mutableStateOf(
|
||||
chapters[chapters.indexOfCurrent(
|
||||
|
|
@ -54,7 +56,7 @@ internal fun ChapterSelectionSheet(
|
|||
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
LaunchedEffect(chapters) {
|
||||
lazyListState.animateScrollToItem(
|
||||
chapters.indexOfCurrent(currentPosition)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import androidx.core.content.getSystemService
|
|||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.TrackSelectionParameters
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.DataSource
|
||||
|
|
@ -76,6 +77,7 @@ internal fun ExoPlayer(
|
|||
|
||||
val audioAttributes = AudioAttributes.Builder()
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
|
||||
.build()
|
||||
|
||||
val renderersFactory = DefaultRenderersFactory(context)
|
||||
|
|
@ -84,6 +86,11 @@ internal fun ExoPlayer(
|
|||
|
||||
val trackSelector = DefaultTrackSelector(context).apply {
|
||||
setParameters(buildUponParameters().apply {
|
||||
setAudioOffloadPreferences(
|
||||
TrackSelectionParameters.AudioOffloadPreferences.DEFAULT.buildUpon().apply {
|
||||
setAudioOffloadMode(TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED)
|
||||
}.build()
|
||||
)
|
||||
setTunnelingEnabled(PlayerSettingsObject.settings.value?.enableTunneling ?: false)
|
||||
setAllowInvalidateSelectionsOnRendererCapabilitiesChange(true)
|
||||
})
|
||||
|
|
@ -100,6 +107,7 @@ internal fun ExoPlayer(
|
|||
.buildWithAssSupport(
|
||||
context,
|
||||
renderersFactory = renderersFactory,
|
||||
extractorsFactory = extractorsFactory,
|
||||
renderType = AssRenderType.LEGACY
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ fun Modifier.highlightOnFocus(
|
|||
): Modifier = composed {
|
||||
var hasFocus by remember { mutableStateOf(false) }
|
||||
val highlightModifier = remember {
|
||||
if (width != 0.dp) {
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(
|
||||
|
|
@ -45,6 +46,14 @@ fun Modifier.highlightOnFocus(
|
|||
color = color.copy(alpha = 0.5f),
|
||||
shape = shape
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(
|
||||
color = color.copy(alpha = 0.25f),
|
||||
shape = shape,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this
|
||||
|
|
|
|||
|
|
@ -219,15 +219,17 @@ class _HorizontalListState extends ConsumerState<HorizontalList> {
|
|||
onFocusChange: (value) {
|
||||
if (value) {
|
||||
final nodesOnSameRow = _nodesInRow(parentNode);
|
||||
final focusNode = lastFocused ?? _firstFullyVisibleNode(context, nodesOnSameRow);
|
||||
final currentNode =
|
||||
nodesOnSameRow.contains(lastFocused) ? lastFocused : _firstFullyVisibleNode(context, nodesOnSameRow);
|
||||
|
||||
if (focusNode != null) {
|
||||
if (currentNode != null) {
|
||||
lastFocused = currentNode;
|
||||
if (widget.onFocused != null) {
|
||||
widget.onFocused!(nodesOnSameRow.indexOf(focusNode));
|
||||
widget.onFocused!(nodesOnSameRow.indexOf(currentNode));
|
||||
} else {
|
||||
context.ensureVisible();
|
||||
}
|
||||
focusNode.requestFocus();
|
||||
currentNode.requestFocus();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -240,7 +242,7 @@ class _HorizontalListState extends ConsumerState<HorizontalList> {
|
|||
child: FocusTraversalGroup(
|
||||
policy: HorizontalRailFocus(
|
||||
parentNode: parentNode,
|
||||
throttle: Throttler(duration: const Duration(milliseconds: 125)),
|
||||
throttle: Throttler(duration: const Duration(milliseconds: 100)),
|
||||
onFocused: (node) {
|
||||
lastFocused = node;
|
||||
final nodesOnSameRow = _nodesInRow(parentNode);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue