package com.example.ui.components import android.view.ViewGroup import androidx.annotation.OptIn import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.CropFree import androidx.compose.material.icons.filled.FastForward import androidx.compose.material.icons.filled.FastRewind import androidx.compose.material.icons.filled.FitScreen import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Pause import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Speed import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState 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.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.media3.common.util.UnstableApi import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView import com.example.data.model.MediaItemData import com.example.data.model.VideoScaleMode import com.example.playback.PlaybackManager import com.example.ui.theme.AccentViolet import com.example.ui.theme.AmoledBlack import com.example.ui.theme.TextPrimary import com.example.ui.theme.TextSecondary import kotlinx.coroutines.delay @OptIn(UnstableApi::class) @Composable fun VideoPlayerView( mediaItem: MediaItemData, modifier: Modifier = Modifier, onBackClick: (() -> Unit)? = null, onInfoClick: (() -> Unit)? = null ) { val context = LocalContext.current val player = remember { PlaybackManager.getOrCreatePlayer(context) } val isPlaying by PlaybackManager.isPlaying.collectAsState() val positionMs by PlaybackManager.currentPositionMs.collectAsState() val durationMs by PlaybackManager.durationMs.collectAsState() val scaleMode by PlaybackManager.videoScaleMode.collectAsState() val speed by PlaybackManager.playbackSpeed.collectAsState() val videoDims by PlaybackManager.videoDimensions.collectAsState() var controlsVisible by remember { mutableStateOf(false) } var isDraggingSlider by remember { mutableStateOf(false) } var dragPosition by remember { mutableStateOf(0f) } // Auto-hide controls after 3.5 seconds of inactivity ONLY while actively playing LaunchedEffect(controlsVisible, isPlaying, isDraggingSlider) { if (controlsVisible && isPlaying && !isDraggingSlider) { delay(3500) controlsVisible = false } } Box( modifier = modifier .fillMaxSize() .background(AmoledBlack) .testTag("video_player_container") ) { // Pure native PlayerView with exact FIT / FILL scaling AndroidView( modifier = Modifier.fillMaxSize(), factory = { ctx -> PlayerView(ctx).apply { layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) useController = false setShutterBackgroundColor(android.graphics.Color.BLACK) setBackgroundColor(android.graphics.Color.BLACK) this.player = player } }, update = { view -> view.player = player view.resizeMode = when (scaleMode) { VideoScaleMode.FIT -> AspectRatioFrameLayout.RESIZE_MODE_FIT VideoScaleMode.FILL -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM } } ) // Full screen gesture overlay sitting on top of PlayerView to capture taps Box( modifier = Modifier .fillMaxSize() .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { PlaybackManager.togglePlayPause() controlsVisible = true } ) // Controls Overlay AnimatedVisibility( visible = controlsVisible, enter = fadeIn(), exit = fadeOut(), modifier = Modifier.fillMaxSize() ) { Box( modifier = Modifier .fillMaxSize() .background(Color.Black.copy(alpha = 0.55f)) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { if (isPlaying) { controlsVisible = false } else { PlaybackManager.togglePlayPause() } } ) { // Top Bar Row( modifier = Modifier .fillMaxWidth() .align(Alignment.TopCenter) .background( Brush.verticalGradient( colors = listOf(Color.Black.copy(alpha = 0.8f), Color.Transparent) ) ) .statusBarsPadding() .padding(horizontal = 16.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { if (onBackClick != null) { IconButton( onClick = onBackClick, modifier = Modifier.testTag("video_back_button") ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", tint = TextPrimary ) } } Column( modifier = Modifier .weight(1f) .padding(horizontal = 8.dp) ) { Text( text = mediaItem.title, color = TextPrimary, style = MaterialTheme.typography.titleMedium, maxLines = 1, overflow = TextOverflow.Ellipsis, fontWeight = FontWeight.SemiBold ) if (videoDims.first > 0 && videoDims.second > 0) { Text( text = "${videoDims.first}x${videoDims.second} • ${mediaItem.formattedSize}", color = TextSecondary, style = MaterialTheme.typography.bodySmall ) } } // FIT / FILL Toggle Button Surface( onClick = { PlaybackManager.toggleVideoScaleMode() }, shape = RoundedCornerShape(8.dp), color = Color.White.copy(alpha = 0.15f), modifier = Modifier.testTag("video_fit_fill_button") ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp) ) { Icon( imageVector = if (scaleMode == VideoScaleMode.FIT) Icons.Default.FitScreen else Icons.Default.CropFree, contentDescription = "Scale Mode", tint = AccentViolet, modifier = Modifier.size(18.dp) ) Spacer(modifier = Modifier.width(4.dp)) Text( text = if (scaleMode == VideoScaleMode.FIT) "FIT" else "FILL", color = TextPrimary, fontSize = 12.sp, fontWeight = FontWeight.Bold ) } } if (onInfoClick != null) { IconButton( onClick = onInfoClick, modifier = Modifier.testTag("video_info_button") ) { Icon( imageVector = Icons.Default.Info, contentDescription = "Video Details", tint = TextPrimary ) } } } // Center Playback Buttons: Seek -10s, Play/Pause, Seek +10s Row( modifier = Modifier .align(Alignment.Center) .fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { IconButton( onClick = { PlaybackManager.seekBy(-10000L) }, modifier = Modifier .size(56.dp) .clip(CircleShape) .background(Color.Black.copy(alpha = 0.4f)) .testTag("video_rewind_button") ) { Icon( imageVector = Icons.Default.FastRewind, contentDescription = "Rewind 10s", tint = TextPrimary, modifier = Modifier.size(32.dp) ) } Spacer(modifier = Modifier.width(32.dp)) IconButton( onClick = { PlaybackManager.togglePlayPause() }, modifier = Modifier .size(72.dp) .clip(CircleShape) .background(AccentViolet) .testTag("video_play_pause_button") ) { Icon( imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, contentDescription = if (isPlaying) "Pause" else "Play", tint = AmoledBlack, modifier = Modifier.size(42.dp) ) } Spacer(modifier = Modifier.width(32.dp)) IconButton( onClick = { PlaybackManager.seekBy(10000L) }, modifier = Modifier .size(56.dp) .clip(CircleShape) .background(Color.Black.copy(alpha = 0.4f)) .testTag("video_forward_button") ) { Icon( imageVector = Icons.Default.FastForward, contentDescription = "Forward 10s", tint = TextPrimary, modifier = Modifier.size(32.dp) ) } } // Bottom Controls: Progress bar, Timestamps, Speed Column( modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter) .background( Brush.verticalGradient( colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.85f)) ) ) .navigationBarsPadding() .padding(horizontal = 16.dp, vertical = 12.dp) ) { val activePos = if (isDraggingSlider) dragPosition.toLong() else positionMs val totalDur = durationMs.coerceAtLeast(1L) Slider( value = activePos.coerceIn(0L, totalDur).toFloat(), onValueChange = { isDraggingSlider = true dragPosition = it }, onValueChangeFinished = { PlaybackManager.seekTo(dragPosition.toLong()) isDraggingSlider = false }, valueRange = 0f..totalDur.toFloat(), colors = SliderDefaults.colors( thumbColor = AccentViolet, activeTrackColor = AccentViolet, inactiveTrackColor = Color.White.copy(alpha = 0.25f) ), modifier = Modifier .fillMaxWidth() .testTag("video_seek_slider") ) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( text = formatTime(activePos), color = TextSecondary, fontSize = 12.sp ) // Playback Speed Selector (0.5x, 1.0x, 1.25x, 1.5x, 2.0x) Surface( onClick = { val nextSpeed = when (speed) { 1.0f -> 1.25f 1.25f -> 1.5f 1.5f -> 2.0f 2.0f -> 0.5f else -> 1.0f } PlaybackManager.setPlaybackSpeed(nextSpeed) }, shape = RoundedCornerShape(6.dp), color = Color.White.copy(alpha = 0.15f), modifier = Modifier.testTag("video_speed_button") ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) ) { Icon( imageVector = Icons.Default.Speed, contentDescription = "Playback Speed", tint = TextSecondary, modifier = Modifier.size(14.dp) ) Spacer(modifier = Modifier.width(4.dp)) Text( text = "${speed}x", color = TextPrimary, fontSize = 12.sp, fontWeight = FontWeight.Medium ) } } Text( text = formatTime(totalDur), color = TextSecondary, fontSize = 12.sp ) } } } } } } fun formatTime(ms: Long): String { if (ms <= 0) return "00:00" val totalSec = ms / 1000 val hours = totalSec / 3600 val minutes = (totalSec % 3600) / 60 val seconds = totalSec % 60 return if (hours > 0) { String.format("%d:%02d:%02d", hours, minutes, seconds) } else { String.format("%02d:%02d", minutes, seconds) } }