Render Android speaker waves independently of local radio mute
This commit is contained in:
1 parent
bcda9b82f4
commit
e95fc090df
2 files changed
+53
-5
No files matched your search
+36
@@ -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)
|
||||
}
|
||||
}
|
||||
+17
-5
@@ -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)) }
|
||||
|
||||
Reference in new issue
Block a user