mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 22:45:20 +00:00
voice note player ui (#680)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user