From 62a976aef53be5a5c43c884bbbf476d1deaf00b7 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 19 Oct 2026 14:01:52 +0200 Subject: [PATCH] Add Android radio call controls and incoming overlay --- .../utun/chat/ui/screens/MemberListScreen.kt | 5 ++ .../utun/chat/ui/screens/RadioCallDialog.kt | 62 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/RadioCallDialog.kt diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt index 60af25a1..f05dc2b6 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt @@ -108,6 +108,7 @@ fun MemberListScreen( isSelected = selectedMember?.nodeId == m.nodeId, onClick = { viewModel.showMemberDetail(channel.id, m.nodeId) }, onCall = { viewModel.startCall(channel.id, m.nodeId, m.name) }, + onRadioCall = { com.utun.chat.data.RadioCallController.start(channel.id, m.nodeId) }, onMessage = { viewModel.startDm(channel.id, m.nodeId, m.name) }, onInvite = { viewModel.showPmInvite(m.nodeId, m.name, channel.id) }, onRadioMute = { viewModel.toggleMemberRadioMute(channel.id, m.nodeId) } @@ -182,6 +183,7 @@ private fun MemberListItem( isSelected: Boolean = false, onClick: () -> Unit, onCall: () -> Unit, + onRadioCall: () -> Unit, onMessage: () -> Unit, onInvite: () -> Unit, onRadioMute: () -> Unit @@ -282,6 +284,9 @@ private fun MemberListItem( IconButton(onClick = onCall, enabled = member.online, modifier = actionModifier) { Icon(Icons.Filled.Call, stringResource(R.string.call)) } + IconButton(onClick = onRadioCall, enabled = member.online, modifier = actionModifier) { + PortableRadioIcon(stringResource(R.string.radio_call_start)) + } IconButton(onClick = onMessage, modifier = actionModifier) { Icon(Icons.AutoMirrored.Filled.Message, stringResource(R.string.message)) } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/RadioCallDialog.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/RadioCallDialog.kt new file mode 100644 index 00000000..e92eafbc --- /dev/null +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/RadioCallDialog.kt @@ -0,0 +1,62 @@ +package com.utun.chat.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.utun.chat.R +import com.utun.chat.data.RadioCallController +import com.utun.chat.data.RadioCallPhase +import com.utun.chat.data.RadioCallState +import com.utun.chat.ui.components.PortableRadioIcon + +/** Как входящий звонок, рисуется внутри Activity, в том числе поверх keyguard. */ +@Composable +fun RadioCallDialog(state: RadioCallState) { + Box(Modifier.fillMaxSize().background(Color(0xCC000000)), contentAlignment = Alignment.Center) { + Surface(shape = MaterialTheme.shapes.large) { + Column(Modifier.padding(28.dp), horizontalAlignment = Alignment.CenterHorizontally) { + PortableRadioIcon(stringResource(R.string.radio_call)) + Text(stringResource(if (state.incoming) R.string.radio_call_incoming else R.string.radio_call)) + Spacer(Modifier.height(16.dp)) + Text(state.peerName, style = MaterialTheme.typography.titleLarge) + Text(stringResource(R.string.radio_call_channel, state.channelName)) + Spacer(Modifier.height(12.dp)) + Text(stringResource(when (state.phase) { + RadioCallPhase.OUTGOING -> R.string.radio_call_checking + RadioCallPhase.INCOMING -> R.string.radio_call_waiting + RadioCallPhase.RINGING -> R.string.radio_call_ringing + RadioCallPhase.PREPARING -> R.string.radio_call_preparing + RadioCallPhase.ENDED -> RadioCallController.reasonText(state.reason) + })) + Spacer(Modifier.height(20.dp)) + if (state.phase != RadioCallPhase.ENDED) { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + TextButton(onClick = { RadioCallController.end(state.id) }) { + Text(stringResource(if (state.phase == RadioCallPhase.INCOMING) R.string.decline else android.R.string.cancel)) + } + if (state.phase == RadioCallPhase.INCOMING) { + Button(onClick = RadioCallController::accept) { Text(stringResource(R.string.accept)) } + } + } + } + } + } + } +}