Browse Source

Render Android speaker waves independently of local radio mute

master
evgeny 5 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.Bolt
import androidx.compose.material.icons.filled.Shield import androidx.compose.material.icons.filled.Shield
import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.automirrored.filled.VolumeUp
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment 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.LinkStatus
import com.utun.chat.data.LocalSocketState import com.utun.chat.data.LocalSocketState
import com.utun.chat.ui.components.PortableRadioIcon import com.utun.chat.ui.components.PortableRadioIcon
import com.utun.chat.ui.components.RadioSpeakerIcon
import com.utun.chat.viewmodel.ChatViewModel import com.utun.chat.viewmodel.ChatViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -70,6 +70,10 @@ fun MemberListScreen(
val localSockets by viewModel.localSockets.collectAsState() val localSockets by viewModel.localSockets.collectAsState()
val channelOwner by viewModel.channelOwner.collectAsState() val channelOwner by viewModel.channelOwner.collectAsState()
val radioMuteFailed by viewModel.radioMuteFailed.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) { LaunchedEffect(selectedMember?.nodeId) {
val nodeId = selectedMember?.nodeId val nodeId = selectedMember?.nodeId
@ -95,6 +99,8 @@ fun MemberListScreen(
MemberListItem( MemberListItem(
member = m, member = m,
group = channel.id, 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, isSelected = selectedMember?.nodeId == m.nodeId,
onClick = { viewModel.showMemberDetail(channel.id, m.nodeId) }, onClick = { viewModel.showMemberDetail(channel.id, m.nodeId) },
onCall = { viewModel.startCall(channel.id, m.nodeId, m.name) }, onCall = { viewModel.startCall(channel.id, m.nodeId, m.name) },
@ -166,6 +172,7 @@ fun MemberListScreen(
private fun MemberListItem( private fun MemberListItem(
member: ChatMember, member: ChatMember,
group: String, group: String,
speaking: Boolean,
isSelected: Boolean = false, isSelected: Boolean = false,
onClick: () -> Unit, onClick: () -> Unit,
onCall: () -> 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() }) { IconToggleButton(checked = member.radioMuted, onCheckedChange = { onRadioMute() }) {
Icon(Icons.AutoMirrored.Filled.VolumeUp, RadioSpeakerIcon(speaking, member.radioMuted, radioStatus + ". " +
stringResource(if (member.radioMuted) R.string.radio_member_listen else R.string.radio_member_mute), stringResource(if (member.radioMuted) R.string.radio_member_listen else R.string.radio_member_mute))
tint = if (member.radioMuted) Color(0xFFEF5350) else Color(0xFF4CAF50))
} }
IconButton(onClick = onInvite) { Icon(Icons.Filled.Add, stringResource(R.string.invite_to_group)) } 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)) } IconButton(onClick = onCall, enabled = member.online) { Icon(Icons.Filled.Call, stringResource(R.string.call)) }

Loading…
Cancel
Save