diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/CustomIconButton.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/CustomIconButton.kt index d7d9941..f0258c3 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/CustomIconButton.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/CustomIconButton.kt @@ -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 { - if (isFocused) { - foreGroundFocusedColor - } else { - foreGroundColor - } - } - } - val currentBackgroundColor by remember { - derivedStateOf { - if (isFocused) { - backgroundFocusedColor - } else { - backgroundColor - } - } - } + val currentContentColor by animateColorAsState( + if (isFocused) { + foreGroundFocusedColor + } else { + foreGroundColor + }, label = "buttonContentColor" + ) + + 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() } } diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/ProgressBar.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/ProgressBar.kt index 8e6a316..6b7ca86 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/ProgressBar.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/ProgressBar.kt @@ -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) ) } } diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/SkipOverlay.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/SkipOverlay.kt index ba6cf7b..f576510 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/SkipOverlay.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/SkipOverlay.kt @@ -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() } diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/VideoEndTime.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/VideoEndTime.kt index 40f8e6d..89bf721 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/VideoEndTime.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/VideoEndTime.kt @@ -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 + ), ) } diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/VideoPlayerControls.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/VideoPlayerControls.kt index b41899f..12cfaa7 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/VideoPlayerControls.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/VideoPlayerControls.kt @@ -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,9 +133,12 @@ fun CustomVideoControls( if (keyEvent.type != KeyEventType.KeyDown) return@onKeyEvent false if (!showControls) { bottomControlFocusRequester.requestFocus() + updateLastInteraction() + return@onKeyEvent true + } else { + updateLastInteraction() + return@onKeyEvent false } - updateLastInteraction() - return@onKeyEvent false } .clickable( indication = null, @@ -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( diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/ChapterSelectionSheet.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/ChapterSelectionSheet.kt index 71b6403..017df1b 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/ChapterSelectionSheet.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/ChapterSelectionSheet.kt @@ -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) ) diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/player/ExoPlayer.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/player/ExoPlayer.kt index 58adb09..e67fc7b 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/player/ExoPlayer.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/player/ExoPlayer.kt @@ -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 ) } diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/utility/Modifiers.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/utility/Modifiers.kt index 9cb42e2..1871444 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/utility/Modifiers.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/utility/Modifiers.kt @@ -34,17 +34,26 @@ fun Modifier.highlightOnFocus( ): Modifier = composed { var hasFocus by remember { mutableStateOf(false) } val highlightModifier = remember { - Modifier - .clip(RoundedCornerShape(8.dp)) - .background( - color = color.copy(alpha = 0.25f), - shape = shape, - ) - .border( - width = width, - color = color.copy(alpha = 0.5f), - shape = shape - ) + if (width != 0.dp) { + Modifier + .clip(RoundedCornerShape(8.dp)) + .background( + color = color.copy(alpha = 0.25f), + shape = shape, + ) + .border( + width = width, + color = color.copy(alpha = 0.5f), + shape = shape + ) + } else { + Modifier + .clip(RoundedCornerShape(8.dp)) + .background( + color = color.copy(alpha = 0.25f), + shape = shape, + ) + } } this diff --git a/lib/widgets/shared/horizontal_list.dart b/lib/widgets/shared/horizontal_list.dart index 68408db..d85022e 100644 --- a/lib/widgets/shared/horizontal_list.dart +++ b/lib/widgets/shared/horizontal_list.dart @@ -219,15 +219,17 @@ class _HorizontalListState extends ConsumerState { 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 { 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);