voice note player ui (#680)

This commit is contained in:
Clare Kinery
2026-03-06 13:07:45 +01:00
committed by GitHub
parent 632ee884e9
commit bc8909bab0
3 changed files with 38 additions and 13 deletions
@@ -1,15 +1,16 @@
package com.bitchat.android.ui.media package com.bitchat.android.ui.media
import android.media.MediaPlayer import android.media.MediaPlayer
import androidx.compose.foundation.background import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.* 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.Icons
import androidx.compose.material.icons.filled.Pause import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
@@ -33,6 +34,8 @@ fun VoiceNotePlayer(
var durationMs by remember { mutableStateOf(0) } var durationMs by remember { mutableStateOf(0) }
val player = remember { MediaPlayer() } val player = remember { MediaPlayer() }
val borderOpacity = if (isSystemInDarkTheme()) 0.3f else 0.2f
// Seek function - position is a fraction from 0.0 to 1.0 // Seek function - position is a fraction from 0.0 to 1.0
val seekTo: (Float) -> Unit = { position -> val seekTo: (Float) -> Unit = { position ->
if (isPrepared && durationMs > 0) { if (isPrepared && durationMs > 0) {
@@ -86,20 +89,34 @@ fun VoiceNotePlayer(
DisposableEffect(Unit) { onDispose { try { player.release() } catch (_: Exception) {} } } DisposableEffect(Unit) { onDispose { try { player.release() } catch (_: Exception) {} } }
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth()
.border(
color = MaterialTheme.colorScheme.primary.copy(alpha = borderOpacity),
width = 1.0.dp,
shape = RoundedCornerShape(14.dp)
)
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
// Disable play/pause while showing send progress override (optional UX choice) // Disable play/pause while showing send progress override (optional UX choice)
val controlsEnabled = isPrepared && !isError && progressOverride == null val controlsEnabled = isPrepared && !isError && progressOverride == null
FilledTonalIconButton(onClick = { if (controlsEnabled) isPlaying = !isPlaying }, enabled = controlsEnabled, modifier = Modifier.size(28.dp)) { FilledTonalIconButton(
onClick = { if (controlsEnabled) isPlaying = !isPlaying },
enabled = controlsEnabled,
modifier = Modifier.size(28.dp),
colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
),
) {
Icon( Icon(
imageVector = if (isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow, imageVector = if (isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow,
contentDescription = if (isPlaying) "Pause" else "Play" contentDescription = if (isPlaying) "Pause" else "Play"
) )
} }
val progressBarColor = progressColor ?: MaterialTheme.colorScheme.primary
com.bitchat.android.ui.media.WaveformPreview( WaveformPreview(
modifier = Modifier modifier = Modifier
.height(24.dp) .height(24.dp)
.weight(1f) .weight(1f)
@@ -110,7 +127,12 @@ fun VoiceNotePlayer(
onSeek = seekTo onSeek = seekTo
) )
val durText = if (durationMs > 0) String.format("%02d:%02d", (durationMs / 1000) / 60, (durationMs / 1000) % 60) else "--:--" val durText = if (durationMs > 0) String.format("%02d:%02d", (durationMs / 1000) / 60, (durationMs / 1000) % 60) else "--:--"
Text(text = durText, fontFamily = FontFamily.Monospace, fontSize = 12.sp) Text(
color = MaterialTheme.colorScheme.tertiary,
text = durText,
fontFamily = FontFamily.Monospace,
fontSize = 12.sp,
)
} }
} }
@@ -11,6 +11,7 @@ import androidx.compose.runtime.withFrameNanos
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
@@ -74,10 +75,10 @@ fun WaveformPreview(
modifier = modifier, modifier = modifier,
samples = stateSamples, samples = stateSamples,
fillProgress = if (stateSamples.isEmpty()) 0f else progress, fillProgress = if (stateSamples.isEmpty()) 0f else progress,
baseColor = Color(0x2200FF7F), baseColor = Color.Gray.copy(alpha = 0.35f),
fillColor = when { fillColor = when {
sendProgress != null -> Color(0xFF1E88E5) // blue while sending sendProgress != null -> Color(0xFF1E88E5) // blue while sending
else -> Color(0xFF00C851) // green during playback else -> MaterialTheme.colorScheme.primary // green during playback
}, },
onSeek = onSeek onSeek = onSeek
) )
@@ -27,7 +27,8 @@ private val DarkColorScheme = darkColorScheme(
surface = Color(0xFF111111), // Very dark gray surface = Color(0xFF111111), // Very dark gray
onSurface = Color(0xFF39FF14), // Green text onSurface = Color(0xFF39FF14), // Green text
error = Color(0xFFFF5555), // Red for errors error = Color(0xFFFF5555), // Red for errors
onError = Color.Black onError = Color.Black,
tertiary = Color(0x99EBEBF5),
) )
private val LightColorScheme = lightColorScheme( private val LightColorScheme = lightColorScheme(
@@ -40,7 +41,8 @@ private val LightColorScheme = lightColorScheme(
surface = Color(0xFFF8F8F8), // Very light gray surface = Color(0xFFF8F8F8), // Very light gray
onSurface = Color(0xFF008000), // Dark green text onSurface = Color(0xFF008000), // Dark green text
error = Color(0xFFCC0000), // Dark red for errors error = Color(0xFFCC0000), // Dark red for errors
onError = Color.White onError = Color.White,
tertiary = Color(0x993C3C43)
) )
@Composable @Composable