Browse Source

Render Android speaker waves independently of local radio mute

master
evgeny 4 days ago
parent
commit
e95fc090df
  1. 36
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/RadioSpeakerIcon.kt
  2. 22
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt

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

@ -0,0 +1,36 @@
package com.utun.chat.ui.components
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
/** Волны показывают речь независимо от mute; красное перечёркивание всегда показывает локальное отключение. */
@Composable
fun RadioSpeakerIcon(speaking: Boolean, muted: Boolean, label: String, modifier: Modifier = Modifier.size(24.dp)) {
val color = when { muted -> Color(0xFFEF5350); speaking -> Color(0xFF4CAF50); else -> Color(0xFF9E9E9E) }
Canvas(modifier.semantics { contentDescription = label }) {
val unit = size.minDimension / 24f
val speaker = Path().apply {
moveTo(4 * unit, 9 * unit); lineTo(8 * unit, 9 * unit); lineTo(13 * unit, 5 * unit)
lineTo(13 * unit, 19 * unit); lineTo(8 * unit, 15 * unit); lineTo(4 * unit, 15 * unit); close()
}
drawPath(speaker, color)
if (speaking) {
val stroke = Stroke(1.8f * unit, cap = StrokeCap.Round)
drawArc(color, -60f, 120f, false, Offset(9 * unit, 7 * unit), Size(10 * unit, 10 * unit), style = stroke)
drawArc(color, -60f, 120f, false, Offset(6 * unit, 4 * unit), Size(16 * unit, 16 * unit), style = stroke)
}
if (muted) drawLine(color, Offset(2 * unit, 22 * unit), Offset(22 * unit, 2 * unit),
strokeWidth = 1.8f * unit, cap = StrokeCap.Round)
}
}

22
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt

@ -27,7 +27,6 @@ import androidx.compose.material.icons.filled.Storage
import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.Shield
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.automirrored.filled.VolumeUp
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -50,6 +49,7 @@ import com.utun.chat.data.MemberLinks
import com.utun.chat.data.LinkStatus
import com.utun.chat.data.LocalSocketState
import com.utun.chat.ui.components.PortableRadioIcon
import com.utun.chat.ui.components.RadioSpeakerIcon
import com.utun.chat.viewmodel.ChatViewModel
@OptIn(ExperimentalMaterial3Api::class)
@ -70,6 +70,10 @@ fun MemberListScreen(
val localSockets by viewModel.localSockets.collectAsState()
val channelOwner by viewModel.channelOwner.collectAsState()
val radioMuteFailed by viewModel.radioMuteFailed.collectAsState()
val radioActive by viewModel.radioActive.collectAsState()
val radioTalkState by viewModel.radioTalkState.collectAsState()
val transmittingGroup by viewModel.radioAudio.transmittingGroup.collectAsState()
val groupId = channel.id.toULongOrNull()?.toLong()
LaunchedEffect(selectedMember?.nodeId) {
val nodeId = selectedMember?.nodeId
@ -95,6 +99,8 @@ fun MemberListScreen(
MemberListItem(
member = m,
group = channel.id,
speaking = radioActive && if (m.isSelf) transmittingGroup == groupId && groupId != null
else radioTalkState.groupId == groupId && m.nodeId in radioTalkState.speakers,
isSelected = selectedMember?.nodeId == m.nodeId,
onClick = { viewModel.showMemberDetail(channel.id, m.nodeId) },
onCall = { viewModel.startCall(channel.id, m.nodeId, m.name) },
@ -166,6 +172,7 @@ fun MemberListScreen(
private fun MemberListItem(
member: ChatMember,
group: String,
speaking: Boolean,
isSelected: Boolean = false,
onClick: () -> Unit,
onCall: () -> Unit,
@ -250,11 +257,16 @@ private fun MemberListItem(
)
}
if (!member.isSelf) {
val radioStatus = stringResource(if (speaking) R.string.radio_member_speaking else R.string.radio_member_silent) +
if (member.radioMuted) " · " + stringResource(R.string.radio_member_muted) else ""
if (member.isSelf) {
Box(Modifier.size(48.dp), contentAlignment = Alignment.Center) {
RadioSpeakerIcon(speaking, false, radioStatus)
}
} else {
IconToggleButton(checked = member.radioMuted, onCheckedChange = { onRadioMute() }) {
Icon(Icons.AutoMirrored.Filled.VolumeUp,
stringResource(if (member.radioMuted) R.string.radio_member_listen else R.string.radio_member_mute),
tint = if (member.radioMuted) Color(0xFFEF5350) else Color(0xFF4CAF50))
RadioSpeakerIcon(speaking, member.radioMuted, radioStatus + ". " +
stringResource(if (member.radioMuted) R.string.radio_member_listen else R.string.radio_member_mute))
}
IconButton(onClick = onInvite) { Icon(Icons.Filled.Add, stringResource(R.string.invite_to_group)) }
IconButton(onClick = onCall, enabled = member.online) { Icon(Icons.Filled.Call, stringResource(R.string.call)) }

Loading…
Cancel
Save