package core.presentation.components import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.rounded.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.PlayerView import androidx.media3.ui.AspectRatioFrameLayout import kotlinx.coroutines.delay import androidx.annotation.OptIn import androidx.media3.common.util.UnstableApi import androidx.compose.foundation.shape.CircleShape import androidx.compose.ui.draw.clip @OptIn(UnstableApi::class) @Composable fun AppVideoPlayer( url: String, modifier: Modifier = Modifier, useController: Boolean = true, autoPlay: Boolean = true, isMuted: Boolean = false, onVideoFinished: () -> Unit = {} ) { val context = LocalContext.current val exoPlayer = remember { ExoPlayer.Builder(context).build().apply { val mediaItem = MediaItem.fromUri(url) setMediaItem(mediaItem) volume = if (isMuted) 0f else 1f prepare() playWhenReady = autoPlay } } var isPlaying by remember { mutableStateOf(autoPlay) } var currentPosition by remember { mutableLongStateOf(0L) } var duration by remember { mutableLongStateOf(0L) } var isControlsVisible by remember { mutableStateOf(true) } var isDragging by remember { mutableStateOf(false) } DisposableEffect(Unit) { val listener = object : Player.Listener { override fun onIsPlayingChanged(playing: Boolean) { isPlaying = playing } override fun onPlaybackStateChanged(state: Int) { if (state == Player.STATE_READY) { duration = exoPlayer.duration } else if (state == Player.STATE_ENDED) { onVideoFinished() } } } exoPlayer.addListener(listener) onDispose { exoPlayer.removeListener(listener) exoPlayer.release() } } LaunchedEffect(isPlaying, isDragging) { if (isPlaying && !isDragging) { while (isPlaying) { currentPosition = exoPlayer.currentPosition delay(500) } } } Box(modifier = modifier.background(Color.Black)) { AndroidView( factory = { PlayerView(it).apply { player = exoPlayer this.useController = false // We use our own UI if needed resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT setBackgroundColor(android.graphics.Color.BLACK) } }, modifier = Modifier.fillMaxSize().clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, enabled = useController // Only clickable if we have controls ) { isControlsVisible = !isControlsVisible } ) AnimatedVisibility( visible = isControlsVisible && useController, enter = fadeIn(), exit = fadeOut() ) { Box( modifier = Modifier .fillMaxSize() .background(Color.Black.copy(alpha = 0.3f)) ) { // Center Controls Row( modifier = Modifier.align(Alignment.Center), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(24.dp) ) { IconButton( onClick = { exoPlayer.seekTo((exoPlayer.currentPosition - 10000).coerceAtLeast(0)) }, modifier = Modifier.size(48.dp) ) { Icon(Icons.Rounded.Replay10, contentDescription = null, tint = Color.White, modifier = Modifier.size(36.dp)) } Box( modifier = Modifier .size(64.dp) .clip(CircleShape) .background(Color.White.copy(alpha = 0.2f)) .clickable { if (isPlaying) exoPlayer.pause() else exoPlayer.play() }, contentAlignment = Alignment.Center ) { Icon( imageVector = if (isPlaying) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, contentDescription = null, tint = Color.White, modifier = Modifier.size(44.dp) ) } IconButton( onClick = { exoPlayer.seekTo((exoPlayer.currentPosition + 10000).coerceAtMost(duration)) }, modifier = Modifier.size(48.dp) ) { Icon(Icons.Rounded.Forward10, contentDescription = null, tint = Color.White, modifier = Modifier.size(36.dp)) } } // Bottom Controls Column( modifier = Modifier .align(Alignment.BottomCenter) .padding(bottom = 16.dp, start = 16.dp, end = 16.dp) ) { Slider( value = currentPosition.toFloat(), onValueChange = { isDragging = true currentPosition = it.toLong() }, onValueChangeFinished = { exoPlayer.seekTo(currentPosition) isDragging = false }, valueRange = 0f..(duration.toFloat().coerceAtLeast(1f)), colors = SliderDefaults.colors( thumbColor = Color.White, activeTrackColor = Color.White, inactiveTrackColor = Color.White.copy(alpha = 0.3f) ), modifier = Modifier.height(24.dp) ) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( text = "${formatTime(currentPosition)} ยท ${formatTime(duration)}", color = Color.White, fontSize = 12.sp ) IconButton(onClick = { /* Settings */ }) { Icon(Icons.Rounded.Settings, contentDescription = null, tint = Color.White, modifier = Modifier.size(20.dp)) } } } } } } } private fun formatTime(ms: Long): String { val totalSeconds = ms / 1000 val minutes = totalSeconds / 60 val seconds = totalSeconds % 60 return String.format("%02d:%02d", minutes, seconds) }