package core.presentation.components import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Pause import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip 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.media3.common.MediaItem import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer import kotlinx.coroutines.delay import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.GraphicEq import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.IntSize @Composable fun AppAudioPlayer( url: String, isVoiceMessage: Boolean = false, modifier: Modifier = Modifier, contentColor: Color = Color.White ) { val context = LocalContext.current val exoPlayer = remember { ExoPlayer.Builder(context).build().apply { val mediaItem = MediaItem.fromUri(url) setMediaItem(mediaItem) prepare() } } var isPlaying by remember { mutableStateOf(false) } var currentPosition by remember { mutableLongStateOf(0L) } var duration by remember { mutableLongStateOf(0L) } var playbackSpeed by remember { mutableFloatStateOf(1.0f) } var layoutSize by remember { mutableStateOf(IntSize.Zero) } val fileName = remember(url) { url.substringAfterLast("/") } 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 } } } exoPlayer.addListener(listener) onDispose { exoPlayer.removeListener(listener) exoPlayer.release() } } LaunchedEffect(isPlaying) { while (isPlaying) { currentPosition = exoPlayer.currentPosition delay(100) } } Column( modifier = modifier .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) .background(Color.Black.copy(alpha = 0.2f)) .padding(8.dp) ) { if (!isVoiceMessage) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(bottom = 4.dp) ) { Icon( Icons.Default.GraphicEq, contentDescription = null, tint = contentColor.copy(alpha = 0.7f), modifier = Modifier.size(16.dp) ) Spacer(modifier = Modifier.width(4.dp)) Text( text = fileName, fontSize = 12.sp, fontWeight = FontWeight.Bold, color = contentColor, maxLines = 1, modifier = Modifier.weight(1f) ) } } Row( verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier .size(40.dp) .clip(CircleShape) .background(Color.White) .clickable { if (isPlaying) exoPlayer.pause() else exoPlayer.play() }, contentAlignment = Alignment.Center ) { Icon( imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, contentDescription = null, tint = Color(0xFF3096E5), modifier = Modifier.size(28.dp) ) } Column( modifier = Modifier .weight(1f) .padding(horizontal = 12.dp) ) { // Waveform / Progress Box( modifier = Modifier .fillMaxWidth() .height(32.dp) .onSizeChanged { layoutSize = it } .pointerInput(duration) { detectTapGestures { offset -> if (duration > 0 && layoutSize.width > 0) { val pct = offset.x / layoutSize.width.toFloat() val newPos = (pct * duration).toLong() exoPlayer.seekTo(newPos) currentPosition = newPos } } }, contentAlignment = Alignment.CenterStart ) { androidx.compose.foundation.Canvas(modifier = Modifier.fillMaxSize()) { val barCount = 35 val barSpacing = 2.dp.toPx() val barWidth = (size.width - (barCount - 1) * barSpacing) / barCount val progress = if (duration > 0) currentPosition.toFloat() / duration.toFloat() else 0f // Fake consistent waveform items val barHeights = listOf( 0.4f, 0.6f, 0.3f, 0.8f, 0.5f, 0.9f, 0.4f, 0.7f, 0.5f, 0.8f, 0.3f, 0.7f, 0.6f, 0.9f, 0.4f, 0.8f, 0.5f, 0.7f, 0.3f, 0.9f, 0.5f, 0.6f, 0.4f, 0.8f, 0.6f, 0.7f, 0.5f, 0.4f, 0.5f, 0.7f, 0.4f, 0.3f, 0.5f, 0.6f, 0.4f ) for (i in 0 until barCount) { val x = i * (barWidth + barSpacing) val heightPct = barHeights[i % barHeights.size] val barHeight = size.height * heightPct val isActive = (i.toFloat() / barCount) <= progress drawRoundRect( color = if (isActive) Color.White else Color.White.copy(alpha = 0.25f), topLeft = androidx.compose.ui.geometry.Offset(x, (size.height - barHeight) / 2), size = androidx.compose.ui.geometry.Size(barWidth, barHeight), cornerRadius = androidx.compose.ui.geometry.CornerRadius(2.dp.toPx()) ) } } } Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { Text( text = "${formatDuration(currentPosition)} / ${formatDuration(duration)}", fontSize = 11.sp, color = contentColor.copy(alpha = 0.7f) ) if (!isVoiceMessage) { Icon( Icons.Default.Download, contentDescription = null, tint = contentColor.copy(alpha = 0.7f), modifier = Modifier.size(14.dp) ) } } } Surface( onClick = { playbackSpeed = when (playbackSpeed) { 1.0f -> 1.5f 1.5f -> 2.0f else -> 1.0f } exoPlayer.playbackParameters = PlaybackParameters(playbackSpeed) }, color = Color.White.copy(alpha = 0.15f), shape = CircleShape, modifier = Modifier.size(36.dp) ) { Box(contentAlignment = Alignment.Center) { Text( text = "${if (playbackSpeed % 1.0f == 0.0f) playbackSpeed.toInt() else playbackSpeed}x", fontSize = 11.sp, fontWeight = FontWeight.Black, color = Color.White ) } } } } } private fun formatDuration(durationMs: Long): String { val totalSeconds = durationMs / 1000 val minutes = totalSeconds / 60 val seconds = totalSeconds % 60 return String.format("%02d:%02d", minutes, seconds) }