diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/burningseries/ui/navigation/screen/video/BottomControls.kt b/composeApp/src/androidMain/kotlin/dev/datlag/burningseries/ui/navigation/screen/video/BottomControls.kt index ba860756..f0fadc07 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/burningseries/ui/navigation/screen/video/BottomControls.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/burningseries/ui/navigation/screen/video/BottomControls.kt @@ -21,7 +21,10 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -36,6 +39,7 @@ import dev.datlag.tooling.compose.platform.ProvideNonTvContentColor import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.StateFlow +import kotlin.math.roundToLong @androidx.annotation.OptIn(UnstableApi::class) @OptIn(ExperimentalMaterial3Api::class) @@ -46,8 +50,6 @@ fun BottomControls( modifier: Modifier = Modifier, ) { val isFinished by playerWrapper.isFinished.collectAsStateWithLifecycle() - val progress by playerWrapper.progress.collectAsStateWithLifecycle() - val length by playerWrapper.length.collectAsStateWithLifecycle() AnimatedVisibility( modifier = modifier.safeDrawingPadding(), @@ -62,39 +64,45 @@ fun BottomControls( ) { ProvideNonTvContentColor { val source = remember { MutableInteractionSource() } - val dragging by source.collectIsDraggedAsState() - var changingProgress by remember { mutableLongStateOf(progress) } - val displayProgress = remember(dragging, progress, changingProgress) { - if (dragging) { - changingProgress - } else { - progress + val isDragging by source.collectIsDraggedAsState() + val position by playerWrapper.progress.collectAsStateWithLifecycle() + val duration by playerWrapper.length.collectAsStateWithLifecycle() + var progress by remember { mutableFloatStateOf(0F) } + val progressForText by remember(progress, duration) { + derivedStateOf { + duration.times(progress).roundToLong() + } + } + + LaunchedEffect(position, duration) { + if (!isDragging) { + progress = if (position <= 0L || duration <= 0L) { + 0F + } else { + position.toFloat() / duration.toFloat() + } } } PlatformText( - text = displayProgress.toDuration(), + text = progressForText.toDuration(), maxLines = 1 ) Slider( modifier = Modifier.weight(1F), - value = displayProgress.toFloat(), - valueRange = 0F..length.toFloat(), + value = progress, onValueChange = { - if (dragging) { - changingProgress = it.toLong() - playerWrapper.showControls() - } + playerWrapper.showControls() + + progress = it }, onValueChangeFinished = { - if (dragging) { - playerWrapper.seekTo(changingProgress) - } + playerWrapper.seekTo(duration.times(progress).roundToLong()) }, interactionSource = source ) PlatformText( - text = length.toDuration(), + text = duration.toDuration(), maxLines = 1 ) } diff --git a/composeApp/src/jvmMain/kotlin/dev/datlag/burningseries/ui/navigation/screen/video/VideoControls.kt b/composeApp/src/jvmMain/kotlin/dev/datlag/burningseries/ui/navigation/screen/video/VideoControls.kt index b928bbc7..52c55cf4 100644 --- a/composeApp/src/jvmMain/kotlin/dev/datlag/burningseries/ui/navigation/screen/video/VideoControls.kt +++ b/composeApp/src/jvmMain/kotlin/dev/datlag/burningseries/ui/navigation/screen/video/VideoControls.kt @@ -1,5 +1,7 @@ package dev.datlag.burningseries.ui.navigation.screen.video +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsDraggedAsState import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.VolumeOff @@ -19,9 +21,12 @@ import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign @@ -32,6 +37,7 @@ import dev.datlag.burningseries.common.toDuration import dev.datlag.tooling.Platform import dev.datlag.tooling.async.scopeCatching import dev.datlag.tooling.compose.platform.colorScheme +import kotlin.math.roundToLong @Composable fun VideoControls(mediaPlayer: MediaPlayer) { @@ -40,16 +46,35 @@ fun VideoControls(mediaPlayer: MediaPlayer) { contentColor = Color.White ) { val isPlaying by mediaPlayer.isPlaying - val time by remember { + val interactionSource = remember { MutableInteractionSource() } + val isDragging by interactionSource.collectIsDraggedAsState() + val position by remember { derivedStateOf { mediaPlayer.time.value } } - val length by remember { + val duration by remember { derivedStateOf { mediaPlayer.length.value } } + var progress by remember { mutableFloatStateOf(0F) } + val progressForText by remember(progress, duration) { + derivedStateOf { + duration.times(progress).roundToLong() + } + } + val isMuted by mediaPlayer.isMuted val window = LocalWindow.current var originalWindowPlacement = remember { window?.placement ?: WindowPlacement.Floating } + LaunchedEffect(position, duration) { + if (!isDragging) { + progress = if (position <= 0L || duration <= 0L) { + 0F + } else { + position.toFloat() / duration.toFloat() + } + } + } + IconButton( onClick = { mediaPlayer.rewind() @@ -89,16 +114,19 @@ fun VideoControls(mediaPlayer: MediaPlayer) { ) } Text( - text = time.toDuration(), + text = progressForText.toDuration(), textAlign = TextAlign.Center ) Slider( modifier = Modifier.weight(1F), - value = time.toDouble().toFloat(), + value = progress, onValueChange = { - mediaPlayer.seekTo(it.toLong()) + progress = it + }, + onValueChangeFinished = { + mediaPlayer.seekTo(duration.times(progress).roundToLong()) }, - valueRange = 0F..length.toDouble().toFloat(), + interactionSource = interactionSource, colors = SliderDefaults.colors( thumbColor = Platform.colorScheme().primary, activeTrackColor = Platform.colorScheme().primary, @@ -106,7 +134,7 @@ fun VideoControls(mediaPlayer: MediaPlayer) { ) ) Text( - text = length.toDuration(), + text = duration.toDuration(), textAlign = TextAlign.Center ) if (window?.placement == WindowPlacement.Fullscreen) { diff --git a/composeApp/src/jvmMain/kotlin/dev/datlag/burningseries/ui/navigation/screen/video/VideoScreen.jvm.kt b/composeApp/src/jvmMain/kotlin/dev/datlag/burningseries/ui/navigation/screen/video/VideoScreen.jvm.kt index 2c6600fe..832fd631 100644 --- a/composeApp/src/jvmMain/kotlin/dev/datlag/burningseries/ui/navigation/screen/video/VideoScreen.jvm.kt +++ b/composeApp/src/jvmMain/kotlin/dev/datlag/burningseries/ui/navigation/screen/video/VideoScreen.jvm.kt @@ -21,7 +21,12 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.awt.SwingPanel @@ -58,14 +63,21 @@ actual fun VideoScreen(component: VideoComponent) { modifier = Modifier.padding(padding).fillMaxSize(), contentAlignment = Alignment.Center ) { + var playReady by remember { mutableStateOf(false) } + SwingPanel( background = Color.Black, modifier = Modifier.fillMaxSize(), - factory = { mediaPlayer.component } + factory = { mediaPlayer.component }, + update = { + playReady = true + } ) - SideEffect { - mediaPlayer.startPlaying() + LaunchedEffect(playReady) { + if (playReady) { + mediaPlayer.startPlaying() + } } } }