Files
forkmessager/client-mobile/core/presentation/components/AppVideoPlayer.kt
Халимов Рустам 2d0bc0d75c Чат, правки
2026-04-14 12:52:03 +03:00

205 lines
7.8 KiB
Kotlin

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)
}