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.material.icons.filled.Download import androidx.compose.material.icons.filled.VolumeUp 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.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 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 kotlinx.coroutines.launch @Composable fun AppAudioPlayer( url: String, name: String? = null, size: Long? = null, isVoiceMessage: Boolean = false, initialPlaybackSpeed: Float = core.utils.PlaybackManager.globalPlaybackSpeed.value, autoPlay: Boolean = false, onFinished: (Float) -> Unit = {}, modifier: Modifier = Modifier, contentColor: Color = Color.White ) { val context = LocalContext.current val scope = rememberCoroutineScope() var playbackSpeed by remember { mutableFloatStateOf(initialPlaybackSpeed) } // Update global speed when changed locally LaunchedEffect(playbackSpeed) { core.utils.PlaybackManager.globalPlaybackSpeed.value = playbackSpeed } val exoPlayer = remember { ExoPlayer.Builder(context).build().apply { val mediaItem = MediaItem.fromUri(url) setMediaItem(mediaItem) playbackParameters = PlaybackParameters(playbackSpeed) val savedPos = core.utils.PlaybackManager.getPosition(url) seekTo(savedPos) prepare() } } // Обработка сигнала авто-запуска LaunchedEffect(autoPlay) { if (autoPlay) { exoPlayer.play() core.utils.PlaybackManager.currentPlayingUrl.value = url } } var isPlaying by remember { mutableStateOf(false) } var currentPosition by remember { mutableLongStateOf(core.utils.PlaybackManager.getPosition(url)) } var duration by remember { mutableLongStateOf(0L) } var layoutSize by remember { mutableStateOf(IntSize.Zero) } // Глобальная синхронизация: если играет кто-то другой, ставим паузу val globalPlayingUrl by core.utils.PlaybackManager.currentPlayingUrl LaunchedEffect(globalPlayingUrl) { if (globalPlayingUrl != url && isPlaying) { exoPlayer.pause() } } val fileName = remember(url, name) { name ?: url.substringAfterLast("/") } DisposableEffect(Unit) { val listener = object : Player.Listener { override fun onIsPlayingChanged(playing: Boolean) { isPlaying = playing if (playing) { core.utils.PlaybackManager.currentPlayingUrl.value = url } } override fun onPlaybackStateChanged(state: Int) { if (state == Player.STATE_READY) { duration = exoPlayer.duration } else if (state == Player.STATE_ENDED) { exoPlayer.pause() // Явно останавливаем, чтобы не было авто-реплея при seekTo if (isVoiceMessage) { exoPlayer.seekTo(0) currentPosition = 0 core.utils.PlaybackManager.resetPosition(url) } onFinished(playbackSpeed) } } } exoPlayer.addListener(listener) onDispose { core.utils.PlaybackManager.savePosition(url, exoPlayer.currentPosition) exoPlayer.removeListener(listener) exoPlayer.release() } } LaunchedEffect(isPlaying) { if (isPlaying) { while (isPlaying) { currentPosition = exoPlayer.currentPosition core.utils.PlaybackManager.savePosition(url, currentPosition) delay(200) } } } Column( modifier = modifier .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) .background(Color.White.copy(alpha = 0.12f)) .padding(8.dp) ) { // Header: Icon and Name Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(bottom = 8.dp) ) { Icon( imageVector = Icons.Default.VolumeUp, contentDescription = null, tint = contentColor.copy(alpha = 0.6f), modifier = Modifier.size(14.dp) ) Spacer(modifier = Modifier.width(6.dp)) Text( text = if (isVoiceMessage) "Голосовое сообщение" else fileName, fontSize = 13.sp, fontWeight = FontWeight.Bold, color = contentColor, maxLines = 1, modifier = Modifier.weight(1f) ) } Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() ) { // Play/Pause Button Box( modifier = Modifier .size(44.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(0xFF3390EC), modifier = Modifier.size(28.dp) ) } Spacer(modifier = Modifier.width(12.dp)) Column(modifier = Modifier.weight(1f)) { // Waveform Box( modifier = Modifier .fillMaxWidth() .height(30.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 = 40 val barSpacing = 2.dp.toPx() val barWidth = (this.size.width - (barCount - 1) * barSpacing) / barCount val progress = if (duration > 0) currentPosition.toFloat() / duration.toFloat() else 0f val barHeights = listOf( 0.3f, 0.5f, 0.2f, 0.7f, 0.4f, 0.8f, 0.3f, 0.6f, 0.4f, 0.7f, 0.2f, 0.6f, 0.5f, 0.8f, 0.3f, 0.7f, 0.4f, 0.6f, 0.2f, 0.8f, 0.4f, 0.5f, 0.3f, 0.7f, 0.5f, 0.6f, 0.4f, 0.3f, 0.4f, 0.6f, 0.3f, 0.2f, 0.4f, 0.5f, 0.3f, 0.6f, 0.4f, 0.7f, 0.3f, 0.5f ) for (i in 0 until barCount) { val x = i * (barWidth + barSpacing) val heightPct = barHeights[i % barHeights.size] val barHeight = this.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, (this.size.height - barHeight) / 2), size = androidx.compose.ui.geometry.Size(barWidth, barHeight), cornerRadius = androidx.compose.ui.geometry.CornerRadius(2.dp.toPx()) ) } } } Spacer(modifier = Modifier.height(2.dp)) // Footer: Time, Size, Download Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { Row(verticalAlignment = Alignment.CenterVertically) { Text( text = formatDuration(if (isPlaying || currentPosition > 0) currentPosition else duration), fontSize = 11.sp, color = contentColor.copy(alpha = 0.7f) ) if (!isVoiceMessage) { size?.let { Spacer(modifier = Modifier.width(12.dp)) val sizeStr = if (it > 1024 * 1024) String.format("%.1f MB", it / (1024.0 * 1024.0)) else "${it / 1024} KB" Text( text = sizeStr, fontSize = 11.sp, fontWeight = FontWeight.Bold, color = contentColor.copy(alpha = 0.7f) ) } } } if (!isVoiceMessage) { Icon( imageVector = Icons.Default.Download, contentDescription = null, tint = contentColor.copy(alpha = 0.6f), modifier = Modifier.size(14.dp).clickable { scope.launch { core.utils.DownloadUtils.downloadMedia(context, url, fileName) } } ) } } } // Speed toggle (only for voice messages or if user wants) if (isVoiceMessage || playbackSpeed > 1.0f) { Spacer(modifier = Modifier.width(8.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.1f), shape = CircleShape, modifier = Modifier.size(32.dp) ) { Box(contentAlignment = Alignment.Center) { Text( text = "${if (playbackSpeed % 1.0f == 0.0f) playbackSpeed.toInt() else playbackSpeed}x", fontSize = 10.sp, fontWeight = FontWeight.Bold, color = Color.White ) } } } } } } private fun formatDuration(durationMs: Long): String { if (durationMs <= 0) return "0:00" val totalSeconds = durationMs / 1000 val minutes = totalSeconds / 60 val seconds = totalSeconds % 60 return String.format("%d:%02d", minutes, seconds) }