Files
forkmessager/client-mobile/core/presentation/components/AppAudioPlayer.kt
Халимов Рустам 3310a3c4a4 Правка чата
2026-04-14 11:31:20 +03:00

238 lines
9.1 KiB
Kotlin

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