Files
forkmessager/client-mobile/core/presentation/components/AppAudioPlayer.kt
T
2026-04-14 10:48:07 +03:00

207 lines
7.3 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
@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) }
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(36.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(24.dp)
)
}
Column(
modifier = Modifier
.weight(1f)
.padding(horizontal = 12.dp)
) {
Slider(
value = if (duration > 0) currentPosition.toFloat() / duration.toFloat() else 0f,
onValueChange = {
val newPos = (it * duration).toLong()
exoPlayer.seekTo(newPos)
currentPosition = newPos
},
modifier = Modifier.height(16.dp),
colors = SliderDefaults.colors(
thumbColor = Color.White,
activeTrackColor = Color.White,
inactiveTrackColor = Color.White.copy(alpha = 0.3f)
)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = formatDuration(currentPosition),
fontSize = 10.sp,
color = contentColor.copy(alpha = 0.7f)
)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "3.3 MB", // Mock size
fontSize = 10.sp,
color = contentColor.copy(alpha = 0.7f)
)
Spacer(modifier = Modifier.width(4.dp))
Icon(
Icons.Default.Download,
contentDescription = null,
tint = contentColor.copy(alpha = 0.7f),
modifier = Modifier.size(12.dp)
)
}
}
}
if (isVoiceMessage) {
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.2f),
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 {
val totalSeconds = durationMs / 1000
val minutes = totalSeconds / 60
val seconds = totalSeconds % 60
return String.format("%02d:%02d", minutes, seconds)
}