Browse Source

voice: fix waveform display + text buffer, 75% width, play/pause state

- bridge: txt buffer 256→2048, limit 250→2000 (voice d-field can be ~900 bytes)
- VoiceBubble: Column layout — play+waveform on top, duration|timestamp on bottom
- Play/pause toggle with ⏸/▶ icons
- Voice messages use 75% screen width max (300dp for others)
topo_upd
evgeny 2 months ago
parent
commit
491fa84db8
  1. 56
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt
  2. 4
      tools/chatgui-android/jni_bridge/android_jni_bridge.c

56
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt

@ -37,6 +37,7 @@ fun MessageBubble(message: Message, onPlayVoice: (String) -> Unit = {}) {
val textColor = if (message.isOutgoing) Color.White else Color.Black
val alignment = if (message.isOutgoing) Arrangement.End else Arrangement.Start
val minBubbleWidth = (LocalConfiguration.current.screenWidthDp / 3).dp
val voiceMaxWidth = (LocalConfiguration.current.screenWidthDp * 0.75f).dp
val isVoice = message.contentType == "audio/opus"
val isMedia = message.contentType.isNotEmpty() && !isVoice && message.contentType != "text/plain"
@ -47,7 +48,7 @@ fun MessageBubble(message: Message, onPlayVoice: (String) -> Unit = {}) {
) {
Column(
modifier = Modifier
.widthIn(min = minBubbleWidth, max = 300.dp)
.widthIn(min = minBubbleWidth, max = if (isVoice) voiceMaxWidth else 300.dp)
.clip(RoundedCornerShape(12.dp))
.background(bgColor)
.padding(12.dp)
@ -65,6 +66,7 @@ fun MessageBubble(message: Message, onPlayVoice: (String) -> Unit = {}) {
}
}
if (!isVoice) {
Spacer(Modifier.height(2.dp))
Text(
formatTime(message.ts),
@ -75,30 +77,40 @@ fun MessageBubble(message: Message, onPlayVoice: (String) -> Unit = {}) {
}
}
}
}
@Composable
private fun VoiceBubble(message: Message, textColor: Color, onPlay: (String) -> Unit) {
val durationSec = message.voiceDurationMs / 1000f
val wf = message.waveform
val wfCount = wf.size
val durMs = message.voiceDurationMs
val durSec = durMs / 1000f
var playing by remember { mutableStateOf(false) }
var elapsedMs by remember { mutableStateOf(0) }
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
/* Play button */
/* Play/Pause button */
Box(
modifier = Modifier.size(32.dp).background(Color(0xFF3390EC), CircleShape)
.clickable { if (message.filePath.isNotEmpty()) onPlay(message.filePath) },
.clickable {
if (!playing && message.filePath.isNotEmpty()) {
playing = true; elapsedMs = 0; onPlay(message.filePath)
} else {
playing = false
}
},
contentAlignment = Alignment.Center
) {
Text("\u25B6", fontSize = 14.sp, color = Color.White)
Text(if (playing) "\u23F8" else "\u25B6", fontSize = 14.sp, color = Color.White)
}
Spacer(Modifier.width(8.dp))
/* Waveform */
if (wfCount >= 2) {
Box(modifier = Modifier.weight(1f).height(32.dp)) {
Canvas(modifier = Modifier.fillMaxSize()) {
Canvas(modifier = Modifier.weight(1f).height(28.dp)) {
val barW = size.width / wfCount
val ph = size.height.toFloat()
val ph = size.height
for (i in 0 until wfCount) {
val v = wf[i]
val db = if (v > 0.00001f) 20f * kotlin.math.log10(v.toDouble()).toFloat() else -100f
@ -107,25 +119,37 @@ private fun VoiceBubble(message: Message, textColor: Color, onPlay: (String) ->
val x = i.toFloat() * barW
val y = ph - barH
drawRect(
color = Color(0xFF3390EC).copy(alpha = 0.5f),
color = Color(0xFF3390EC).copy(alpha = 0.35f),
topLeft = Offset(x, y),
size = Size(barW * 0.8f, barH)
size = Size(barW * 0.75f, barH)
)
}
}
}
Spacer(Modifier.width(4.dp))
}
/* Duration */
Spacer(Modifier.height(4.dp))
/* Duration + timestamp */
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom
) {
Text(
formatDurationMs(durMs),
fontSize = 12.sp,
color = textColor.copy(alpha = 0.55f)
)
Text(
formatDurationMs(message.voiceDurationMs),
fontSize = 13.sp,
color = textColor.copy(alpha = 0.7f),
modifier = Modifier.padding(start = 4.dp)
formatTime(message.ts),
fontSize = 11.sp,
color = textColor.copy(alpha = 0.4f),
modifier = Modifier.align(Alignment.CenterVertically).padding(start = 8.dp)
)
}
}
}
private fun formatDurationMs(ms: Int): String {
val secs = ms / 1000

4
tools/chatgui-android/jni_bridge/android_jni_bridge.c

@ -377,10 +377,10 @@ char* utun_bridge_get_messages_json(const char* channel_id, int limit) {
int dlen = sqlite3_column_bytes(st, 2);
const char* lattr = (const char*)sqlite3_column_text(st, 4);
char txt[256] = "", ct[64] = "text/plain";
char txt[2048] = "", ct[64] = "text/plain";
if (dptr && dlen > 0) {
const char* ds = strstr((const char*)dptr, "\"d\":\"");
if (ds) { ds += 5; char* de = strchr((char*)ds, '"'); if (de) { size_t tl = (size_t)(de - ds); if (tl > 250) tl = 250; memcpy(txt, ds, tl); txt[tl] = '\0'; } }
if (ds) { ds += 5; char* de = strchr((char*)ds, '"'); if (de) { size_t tl = (size_t)(de - ds); if (tl > 2000) tl = 2000; memcpy(txt, ds, tl); txt[tl] = '\0'; } }
const char* cts = strstr((const char*)dptr, "\"ct\":\"");
if (cts) { cts += 6; char* cte = strchr((char*)cts, '"'); if (cte) { size_t tlc = (size_t)(cte - cts); if (tlc < sizeof(ct)) { memcpy(ct, cts, tlc); ct[tlc] = '\0'; } } }
}

Loading…
Cancel
Save