Browse Source

Add unified Android dark theme and improve UI contrast

master
evgeny 1 day ago
parent
commit
752c1f140c
  1. 9
      tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt
  2. 2
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ActiveCallBanner.kt
  3. 43
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt
  4. 14
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallScreen.kt
  5. 10
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt
  6. 4
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt
  7. 8
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/IncomingCallDialog.kt
  8. 12
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/LogScreen.kt
  9. 38
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt
  10. 12
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/PhotoRecordScreen.kt
  11. 6
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt
  12. 6
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/VideoPlayerScreen.kt
  13. 16
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/VideoRecordScreen.kt
  14. 50
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/theme/Theme.kt
  15. 10
      tools/chatgui-android/app/src/main/res/values/themes.xml

9
tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt

@ -35,6 +35,7 @@ import com.utun.chat.data.Message
import com.utun.chat.data.NativeLib import com.utun.chat.data.NativeLib
import com.utun.chat.data.RadioAudioEngine import com.utun.chat.data.RadioAudioEngine
import com.utun.chat.headless.HeadlessService import com.utun.chat.headless.HeadlessService
import com.utun.chat.ui.theme.VibeChatTheme
import com.utun.chat.ui.components.ActiveCallBanner import com.utun.chat.ui.components.ActiveCallBanner
import com.utun.chat.ui.screens.* import com.utun.chat.ui.screens.*
import com.utun.chat.viewmodel.ChatViewModel import com.utun.chat.viewmodel.ChatViewModel
@ -58,10 +59,10 @@ class MainActivity : ComponentActivity() {
showOverLockScreen() showOverLockScreen()
} }
/* Edge-to-edge with dark status/navigation bar icons (light app theme) */ /* Светлые системные значки на тёмном фоне приложения. */
enableEdgeToEdge( enableEdgeToEdge(
statusBarStyle = SystemBarStyle.light(android.graphics.Color.TRANSPARENT, android.graphics.Color.TRANSPARENT), statusBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT),
navigationBarStyle = SystemBarStyle.light(android.graphics.Color.TRANSPARENT, android.graphics.Color.TRANSPARENT) navigationBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT)
) )
/* Start uTun headless service (must be from foreground Activity on API 34+). /* Start uTun headless service (must be from foreground Activity on API 34+).
@ -71,6 +72,7 @@ class MainActivity : ComponentActivity() {
} }
setContent { setContent {
VibeChatTheme {
val vm: ChatViewModel = viewModel(factory = ChatViewModel.Factory()) val vm: ChatViewModel = viewModel(factory = ChatViewModel.Factory())
/* Pause GUI polling (health-check, recording/playback timers) when backgrounded, /* Pause GUI polling (health-check, recording/playback timers) when backgrounded,
@ -356,6 +358,7 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
}
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()

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

@ -83,7 +83,7 @@ fun ActiveCallBanner(
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Box( Box(
modifier = Modifier.size(40.dp).background(Color(0xFFE53935), CircleShape), modifier = Modifier.size(40.dp).background(Color(0xFFB32632), CircleShape),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
IconButton(onClick = onHangup) { IconButton(onClick = onHangup) {

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

@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import com.utun.chat.ui.theme.ChatColors
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
@ -82,8 +83,8 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState =
onOpenFile: (Message) -> Unit = {}, onOpenFile: (Message) -> Unit = {},
onOpenImage: (Message) -> Unit = {}, onOpenImage: (Message) -> Unit = {},
onPlayVideo: (Message) -> Unit = {}) { onPlayVideo: (Message) -> Unit = {}) {
val bgColor = if (message.isOutgoing) Color(0xFF4A90D9) else Color(0xFFE8E8E8) val bgColor = if (message.isOutgoing) ChatColors.Outgoing else ChatColors.Raised
val textColor = if (message.isOutgoing) Color.White else Color.Black val textColor = ChatColors.Text
val alignment = if (message.isOutgoing) Arrangement.End else Arrangement.Start val alignment = if (message.isOutgoing) Arrangement.End else Arrangement.Start
val minBubbleWidth = (LocalConfiguration.current.screenWidthDp / 3).dp val minBubbleWidth = (LocalConfiguration.current.screenWidthDp / 3).dp
val voiceMaxWidth = (LocalConfiguration.current.screenWidthDp * 0.75f).dp val voiceMaxWidth = (LocalConfiguration.current.screenWidthDp * 0.75f).dp
@ -120,7 +121,7 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState =
.background(bgColor) .background(bgColor)
.padding(12.dp) .padding(12.dp)
) { ) {
if (!isVoice) Text(message.author, fontSize = 13.sp, fontWeight = FontWeight.Bold, color = textColor.copy(alpha = 0.7f)) if (!isVoice) Text(message.author, fontSize = 13.sp, fontWeight = FontWeight.Bold, color = ChatColors.Secondary)
Spacer(Modifier.height(2.dp)) Spacer(Modifier.height(2.dp))
when { when {
@ -138,7 +139,7 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState =
Text( Text(
formatTime(message.ts), formatTime(message.ts),
fontSize = 11.sp, fontSize = 11.sp,
color = textColor.copy(alpha = 0.5f), color = ChatColors.Secondary,
modifier = Modifier.align(Alignment.End) modifier = Modifier.align(Alignment.End)
) )
} }
@ -164,7 +165,7 @@ private fun VoiceBubble(message: Message, textColor: Color, isActive: Boolean,
Column { Column {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Box( Box(
modifier = Modifier.size(32.dp).background(Color.White, CircleShape) modifier = Modifier.size(32.dp).background(ChatColors.Background, CircleShape)
.clickable { .clickable {
when { when {
downloading -> {} downloading -> {}
@ -181,7 +182,7 @@ private fun VoiceBubble(message: Message, textColor: Color, isActive: Boolean,
playing && !paused -> "\u23F8" playing && !paused -> "\u23F8"
else -> "\u25B6" else -> "\u25B6"
}, },
fontSize = 14.sp, color = Color.Black fontSize = 14.sp, color = ChatColors.Text
) )
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
@ -212,7 +213,7 @@ private fun VoiceBubble(message: Message, textColor: Color, isActive: Boolean,
val y = ph - barH val y = ph - barH
val played = showCursor && i < playedIdx val played = showCursor && i < playedIdx
drawRect( drawRect(
color = when { played -> Color.White; else -> Color(0xFFCCCCD4).copy(alpha = 0.8f) }, color = if (played) ChatColors.Accent else ChatColors.Secondary,
topLeft = Offset(x, y), topLeft = Offset(x, y),
size = Size(barW, barH) size = Size(barW, barH)
) )
@ -241,12 +242,12 @@ private fun VoiceBubble(message: Message, textColor: Color, isActive: Boolean,
else -> formatDurationMs(durMs) else -> formatDurationMs(durMs)
}, },
fontSize = 12.sp, fontSize = 12.sp,
color = textColor.copy(alpha = 0.55f) color = ChatColors.Secondary
) )
Text( Text(
formatTime(message.ts), formatTime(message.ts),
fontSize = 11.sp, fontSize = 11.sp,
color = textColor.copy(alpha = 0.4f), color = ChatColors.Secondary,
modifier = Modifier.align(Alignment.CenterVertically).padding(start = 8.dp) modifier = Modifier.align(Alignment.CenterVertically).padding(start = 8.dp)
) )
} }
@ -287,7 +288,7 @@ private fun FileBubble(message: Message, textColor: Color, onDownload: (Message)
Text( Text(
"${formatFileSize(message.fileSize)} · $stateText", "${formatFileSize(message.fileSize)} · $stateText",
fontSize = 12.sp, fontSize = 12.sp,
color = textColor.copy(alpha = 0.5f) color = ChatColors.Secondary
) )
} }
} }
@ -357,7 +358,7 @@ private fun VideoBubble(message: Message, textColor: Color,
) { ) {
thumb?.let { Image(it.asImageBitmap(), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize()) } thumb?.let { Image(it.asImageBitmap(), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize()) }
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Box(modifier = Modifier.size(44.dp).background(Color(0x88000000), CircleShape), contentAlignment = Alignment.Center) { Box(modifier = Modifier.size(44.dp).background(Color(0xCC000000), CircleShape), contentAlignment = Alignment.Center) {
Text( Text(
when { downloading -> "\u23F3"; isError -> "\u267B"; !downloaded -> "\u2B07"; else -> "\u25B6" }, when { downloading -> "\u23F3"; isError -> "\u267B"; !downloaded -> "\u2B07"; else -> "\u25B6" },
fontSize = 18.sp, color = Color.White fontSize = 18.sp, color = Color.White
@ -368,7 +369,7 @@ private fun VideoBubble(message: Message, textColor: Color,
Text( Text(
formatDurationMs(durMs), fontSize = 11.sp, color = Color.White, formatDurationMs(durMs), fontSize = 11.sp, color = Color.White,
modifier = Modifier.align(Alignment.BottomEnd).padding(6.dp) modifier = Modifier.align(Alignment.BottomEnd).padding(6.dp)
.background(Color(0x88000000), RoundedCornerShape(4.dp)) .background(Color(0xCC000000), RoundedCornerShape(4.dp))
.padding(horizontal = 4.dp, vertical = 1.dp) .padding(horizontal = 4.dp, vertical = 1.dp)
) )
} }
@ -392,7 +393,7 @@ private fun VideoBubble(message: Message, textColor: Color,
else -> "Tap to play" else -> "Tap to play"
}) })
}.trimEnd(), }.trimEnd(),
fontSize = 12.sp, color = textColor.copy(alpha = 0.5f) fontSize = 12.sp, color = ChatColors.Secondary
) )
} }
} }
@ -434,7 +435,7 @@ private fun ImageBubble(message: Message, textColor: Color,
) { ) {
bmp?.let { Image(it.asImageBitmap(), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize()) } bmp?.let { Image(it.asImageBitmap(), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize()) }
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Box(modifier = Modifier.size(44.dp).background(Color(0x88000000), CircleShape), contentAlignment = Alignment.Center) { Box(modifier = Modifier.size(44.dp).background(Color(0xCC000000), CircleShape), contentAlignment = Alignment.Center) {
Text( Text(
when { downloading -> "\u23F3"; isError -> "\u267B"; !downloaded -> "\u2B07"; else -> "" }, when { downloading -> "\u23F3"; isError -> "\u267B"; !downloaded -> "\u2B07"; else -> "" },
fontSize = 18.sp, color = Color.White fontSize = 18.sp, color = Color.White
@ -453,7 +454,7 @@ private fun ImageBubble(message: Message, textColor: Color,
!downloaded -> "Tap to download" !downloaded -> "Tap to download"
else -> "Tap to open" else -> "Tap to open"
}, },
fontSize = 12.sp, color = textColor.copy(alpha = 0.5f) fontSize = 12.sp, color = ChatColors.Secondary
) )
} }
} }
@ -548,7 +549,7 @@ fun InputBar(
Text( Text(
formatFileSize(pendingAttachmentSize), formatFileSize(pendingAttachmentSize),
fontSize = 12.sp, fontSize = 12.sp,
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.6f) color = ChatColors.Secondary
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
} }
@ -577,7 +578,7 @@ fun InputBar(
Surface( Surface(
modifier = Modifier.weight(1f).height(48.dp), modifier = Modifier.weight(1f).height(48.dp),
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
color = Color(0xFFE53935) color = Color(0xFF54252A)
) { ) {
Row( Row(
modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp), modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp),
@ -615,10 +616,10 @@ fun InputBar(
if (isRecording && isRecordingLocked) { if (isRecording && isRecordingLocked) {
/* Locked: cancel + send voice */ /* Locked: cancel + send voice */
IconButton(onClick = onVoiceCancel) { IconButton(onClick = onVoiceCancel) {
Text("\u2715", fontSize = 20.sp, color = Color(0xFFE53935)) Text("\u2715", fontSize = 20.sp, color = Color(0xFFFFB4AB))
} }
IconButton(onClick = onVoiceSend) { IconButton(onClick = onVoiceSend) {
Text("\u25B6", fontSize = 20.sp, color = Color(0xFF4CAF50)) Text("\u25B6", fontSize = 20.sp, color = Color(0xFF81C784))
} }
} else if (!isRecording && (text.isNotBlank() || hasAttachment)) { } else if (!isRecording && (text.isNotBlank() || hasAttachment)) {
/* Send button — round, primary color */ /* Send button — round, primary color */
@ -630,11 +631,11 @@ fun InputBar(
.clickable { onSend() }, .clickable { onSend() },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text("\u27A4", fontSize = 22.sp, color = Color.White) Text("\u27A4", fontSize = 22.sp, color = MaterialTheme.colorScheme.onPrimary)
} }
} else { } else {
/* Unified mode button — short tap toggles voice/video, hold activates */ /* Unified mode button — short tap toggles voice/video, hold activates */
val pttBg = if (isRecording) Color(0xFFE53935) else Color.Transparent val pttBg = if (isRecording) Color(0xFFB32632) else Color.Transparent
Box( Box(
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)

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

@ -84,7 +84,7 @@ private fun CallControlButton(
} }
} }
Spacer(Modifier.height(6.dp)) Spacer(Modifier.height(6.dp))
Text(label, color = if (active) Color.White else Color(0xFF9BA6AD), fontSize = 11.sp) Text(label, color = if (active) Color.White else Color(0xFFAEBCC8), fontSize = 11.sp)
} }
} }
@ -168,7 +168,7 @@ fun CallScreen(
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Text( Text(
if (phase == CallPhase.ACTIVE) fmtDuration(duration) else callStatusText(phase), if (phase == CallPhase.ACTIVE) fmtDuration(duration) else callStatusText(phase),
color = Color(0xFF9BA6AD), color = Color(0xFFAEBCC8),
fontSize = 16.sp fontSize = 16.sp
) )
val bgpPath = st?.bgpPath ?: "" val bgpPath = st?.bgpPath ?: ""
@ -176,7 +176,7 @@ fun CallScreen(
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Text( Text(
bgpPath, bgpPath,
color = Color(0xFF9BA6AD), color = Color(0xFFAEBCC8),
fontSize = 13.sp fontSize = 13.sp
) )
} }
@ -231,13 +231,13 @@ fun CallScreen(
contentDescription = if (muted) "Включить микрофон" else "Выключить микрофон", contentDescription = if (muted) "Включить микрофон" else "Выключить микрофон",
label = if (muted) "Вкл. микрофон" else "Mute", label = if (muted) "Вкл. микрофон" else "Mute",
active = muted, active = muted,
activeColor = Color(0xFFE53935), activeColor = Color(0xFFB32632),
onClick = { viewModel.toggleMute() } onClick = { viewModel.toggleMute() }
) )
} }
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box( Box(
modifier = Modifier.size(72.dp).background(Color(0xFFE53935), CircleShape), modifier = Modifier.size(72.dp).background(Color(0xFFB32632), CircleShape),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
IconButton(onClick = { viewModel.hangupCall() }) { IconButton(onClick = { viewModel.hangupCall() }) {
@ -245,7 +245,7 @@ fun CallScreen(
} }
} }
Spacer(Modifier.height(6.dp)) Spacer(Modifier.height(6.dp))
Text("Завершить", color = Color(0xFF9BA6AD), fontSize = 12.sp) Text("Завершить", color = Color(0xFFAEBCC8), fontSize = 12.sp)
} }
} }
} }
@ -268,7 +268,7 @@ fun CallScreen(
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Text("Экран заблокирован", color = Color.White, fontSize = 20.sp, fontWeight = FontWeight.Medium) Text("Экран заблокирован", color = Color.White, fontSize = 20.sp, fontWeight = FontWeight.Medium)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Text("Отведите телефон от уха, чтобы разблокировать", color = Color(0xFF9BA6AD), fontSize = 14.sp) Text("Отведите телефон от уха, чтобы разблокировать", color = Color(0xFFAEBCC8), fontSize = 14.sp)
} }
} }
} }

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

@ -59,7 +59,7 @@ fun ChannelListScreen(
actions = { actions = {
Box( Box(
Modifier.size(10.dp) Modifier.size(10.dp)
.background(if (isStuck) Color.Red else Color(0xFF4CAF50), CircleShape) .background(if (isStuck) Color(0xFFFFB4AB) else Color(0xFF81C784), CircleShape)
) )
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
IconButton(onClick = onJoinChannel) { Icon(Icons.Default.QrCode, "Join") } IconButton(onClick = onJoinChannel) { Icon(Icons.Default.QrCode, "Join") }
@ -86,7 +86,7 @@ fun ChannelListScreen(
} else { } else {
LazyColumn { LazyColumn {
items(channels) { ch -> items(channels) { ch ->
val barColor = if (ch.peersOnline > 0) Color(0xFF4CAF50) else Color(0xFF9E9E9E) val barColor = if (ch.peersOnline > 0) Color(0xFF81C784) else Color(0xFFAEBCC8)
val badgeText = when { val badgeText = when {
ch.peersOnline > 99 -> "99+" ch.peersOnline > 99 -> "99+"
ch.peersOnline > 0 -> ch.peersOnline.toString() ch.peersOnline > 0 -> ch.peersOnline.toString()
@ -126,7 +126,7 @@ fun ChannelListScreen(
.offset(x = (-4).dp, y = (-6).dp) .offset(x = (-4).dp, y = (-6).dp)
.size(badgeSize) .size(badgeSize)
.clip(CircleShape) .clip(CircleShape)
.background(Color(0xFF4CAF50)), .background(Color(0xFF24633D)),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
@ -164,10 +164,10 @@ fun ChannelListScreen(
items(dmConversations) { dm -> items(dmConversations) { dm ->
Surface(modifier = Modifier.clickable { onSelectDm(dm) }) { Surface(modifier = Modifier.clickable { onSelectDm(dm) }) {
Row(Modifier.fillMaxWidth().height(IntrinsicSize.Min)) { Row(Modifier.fillMaxWidth().height(IntrinsicSize.Min)) {
Box(Modifier.width(4.dp).fillMaxHeight().background(Color(0xFF8E44AD))) Box(Modifier.width(4.dp).fillMaxHeight().background(Color(0xFFD2A8FF)))
Row(Modifier.fillMaxWidth().padding(12.dp)) { Row(Modifier.fillMaxWidth().padding(12.dp)) {
Box(Modifier.size(44.dp), contentAlignment = Alignment.Center) { Box(Modifier.size(44.dp), contentAlignment = Alignment.Center) {
Text("@", fontSize = 22.sp, color = Color(0xFF8E44AD)) Text("@", fontSize = 22.sp, color = Color(0xFFD2A8FF))
} }
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(10.dp))
Column { Column {

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

@ -366,7 +366,7 @@ private fun RadioPttButton(
} }
}, },
shape = CircleShape, shape = CircleShape,
color = if (isTalking) Color(0xFFE53935) else Color(0xFF81C784), color = if (isTalking) Color(0xFFFFB4AB) else Color(0xFF81C784),
shadowElevation = 6.dp shadowElevation = 6.dp
) { ) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
@ -459,7 +459,7 @@ private fun RadioControlButton(
onCheckedChange = { onClick() }, onCheckedChange = { onClick() },
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
colors = IconButtonDefaults.iconToggleButtonColors( colors = IconButtonDefaults.iconToggleButtonColors(
containerColor = Color(0xFF24323D), contentColor = Color(0xFF9BA6AD), containerColor = Color(0xFF24323D), contentColor = Color(0xFFAEBCC8),
checkedContainerColor = Color(0xFF1F6FEB), checkedContentColor = Color.White) checkedContainerColor = Color(0xFF1F6FEB), checkedContentColor = Color.White)
) { ) {
if (floating) { if (floating) {

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

@ -49,20 +49,20 @@ fun IncomingCallDialog(
Spacer(Modifier.height(20.dp)) Spacer(Modifier.height(20.dp))
Text(name, color = Color.White, fontSize = 22.sp, fontWeight = FontWeight.Medium) Text(name, color = Color.White, fontSize = 22.sp, fontWeight = FontWeight.Medium)
Spacer(Modifier.height(6.dp)) Spacer(Modifier.height(6.dp))
Text("Входящий звонок…", color = Color(0xFF9BA6AD), fontSize = 14.sp) Text("Входящий звонок…", color = Color(0xFFAEBCC8), fontSize = 14.sp)
Spacer(Modifier.height(28.dp)) Spacer(Modifier.height(28.dp))
Row(horizontalArrangement = Arrangement.spacedBy(48.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(48.dp)) {
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box( Box(
modifier = Modifier.size(64.dp).background(Color(0xFFE53935), CircleShape), modifier = Modifier.size(64.dp).background(Color(0xFFB32632), CircleShape),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
IconButton(onClick = { viewModel.declineCall() }) { IconButton(onClick = { viewModel.declineCall() }) {
Icon(Icons.Filled.CallEnd, "Отклонить", tint = Color.White, modifier = Modifier.size(30.dp)) Icon(Icons.Filled.CallEnd, "Отклонить", tint = Color.White, modifier = Modifier.size(30.dp))
} }
} }
Text("Отклонить", color = Color(0xFF9BA6AD), fontSize = 12.sp) Text("Отклонить", color = Color(0xFFAEBCC8), fontSize = 12.sp)
} }
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box( Box(
@ -73,7 +73,7 @@ fun IncomingCallDialog(
Icon(Icons.Filled.Call, "Принять", tint = Color.White, modifier = Modifier.size(30.dp)) Icon(Icons.Filled.Call, "Принять", tint = Color.White, modifier = Modifier.size(30.dp))
} }
} }
Text("Принять", color = Color(0xFF9BA6AD), fontSize = 12.sp) Text("Принять", color = Color(0xFFAEBCC8), fontSize = 12.sp)
} }
} }
} }

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

@ -52,7 +52,7 @@ fun LogScreen(onBack: () -> Unit) {
val selected = filterLevel == lvl val selected = filterLevel == lvl
Text( Text(
label, label,
color = if (selected) Color.White else MaterialTheme.colorScheme.onSurfaceVariant, color = if (selected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 11.sp, fontSize = 11.sp,
modifier = Modifier modifier = Modifier
.clickable { filterLevel = lvl } .clickable { filterLevel = lvl }
@ -77,12 +77,12 @@ fun LogScreen(onBack: () -> Unit) {
items(filtered.size) { idx -> // index-based to avoid recomposition on each new entry items(filtered.size) { idx -> // index-based to avoid recomposition on each new entry
val entry = filtered[idx] val entry = filtered[idx]
val color = when (entry.level) { val color = when (entry.level) {
"ERROR" -> Color(0xFFFF4444) "ERROR" -> Color(0xFFFFB4AB)
"WARN" -> Color(0xFFFFAA00) "WARN" -> Color(0xFFFFAA00)
"INFO" -> Color(0xFF4CAF50) "INFO" -> Color(0xFF81C784)
"DEBUG" -> Color(0xFF2196F3) "DEBUG" -> Color(0xFF79B8FF)
"TRACE" -> Color(0xFF9E9E9E) "TRACE" -> Color(0xFFAEBCC8)
else -> Color.Gray else -> Color(0xFFAEBCC8)
} }
Text( Text(
text = "${entry.level} [${entry.category}] ${entry.message}", text = "${entry.level} [${entry.category}] ${entry.message}",

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

@ -143,8 +143,8 @@ private fun MemberListItem(
) { ) {
val onlineColor = when { val onlineColor = when {
member.connecting -> Color(0xFFFFEB3B) // yellow: connecting member.connecting -> Color(0xFFFFEB3B) // yellow: connecting
member.online -> Color(0xFF4CAF50) // green: reachable (direct/indirect/relay) member.online -> Color(0xFF81C784) // green: reachable (direct/indirect/relay)
else -> Color(0xFF9E9E9E) // gray: offline else -> Color(0xFFAEBCC8) // gray: offline
} }
val bgColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else Color.Transparent val bgColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else Color.Transparent
@ -160,7 +160,7 @@ private fun MemberListItem(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
if (member.isSelf) { if (member.isSelf) {
Text("✓", fontSize = 12.sp, color = Color(0xFF4CAF50)) Text("✓", fontSize = 12.sp, color = Color(0xFF81C784))
} else { } else {
Box( Box(
modifier = Modifier modifier = Modifier
@ -182,7 +182,7 @@ private fun MemberListItem(
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
} }
if (member.isModer) { if (member.isModer) {
Icon(Icons.Filled.Shield, "Moderator", tint = Color(0xFF2196F3), modifier = Modifier.size(16.dp)) Icon(Icons.Filled.Shield, "Moderator", tint = Color(0xFF79B8FF), modifier = Modifier.size(16.dp))
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
} }
Text( Text(
@ -192,7 +192,7 @@ private fun MemberListItem(
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
textDecoration = if (member.isDeleted) TextDecoration.LineThrough else TextDecoration.None, textDecoration = if (member.isDeleted) TextDecoration.LineThrough else TextDecoration.None,
color = if (member.isSelf) Color(0xFF4CAF50) else Color.Unspecified, color = if (member.isSelf) Color(0xFF81C784) else Color.Unspecified,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
} }
@ -215,7 +215,7 @@ private fun MemberListItem(
text = parts.joinToString(" "), text = parts.joinToString(" "),
fontSize = 11.sp, fontSize = 11.sp,
color = if (member.isSelf || member.online) color = if (member.isSelf || member.online)
Color(0xFF4CAF50) else Color.Gray Color(0xFF81C784) else Color(0xFFAEBCC8)
) )
} }
} }
@ -260,7 +260,7 @@ private fun MemberDetailCard(
) { ) {
Column(modifier = Modifier.padding(12.dp).verticalScroll(rememberScrollState())) { Column(modifier = Modifier.padding(12.dp).verticalScroll(rememberScrollState())) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
val dotColor = if (detail.online) Color(0xFF4CAF50) else Color(0xFF9E9E9E) val dotColor = if (detail.online) Color(0xFF81C784) else Color(0xFFAEBCC8)
Box( Box(
modifier = Modifier modifier = Modifier
.size(10.dp) .size(10.dp)
@ -271,7 +271,7 @@ private fun MemberDetailCard(
Text(detail.name, fontWeight = FontWeight.Bold, fontSize = 15.sp) Text(detail.name, fontWeight = FontWeight.Bold, fontSize = 15.sp)
if (isSelf) { if (isSelf) {
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
Text("✓", fontSize = 12.sp, color = Color(0xFF4CAF50)) Text("✓", fontSize = 12.sp, color = Color(0xFF81C784))
} }
} }
@ -299,7 +299,7 @@ private fun MemberDetailCard(
text = headerParts.joinToString(" · "), text = headerParts.joinToString(" · "),
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontSize = 11.sp, fontSize = 11.sp,
color = Color.Gray color = Color(0xFFAEBCC8)
) )
if (isSelf) { if (isSelf) {
@ -307,7 +307,7 @@ private fun MemberDetailCard(
Spacer(Modifier.height(6.dp)) Spacer(Modifier.height(6.dp))
Text("Listen sockets:", fontSize = 13.sp, fontWeight = FontWeight.Medium) Text("Listen sockets:", fontSize = 13.sp, fontWeight = FontWeight.Medium)
localSocketStates.forEach { sk -> localSocketStates.forEach { sk ->
val dot = if (sk.state == 2) Color(0xFF4CAF50) else Color(0xFF9E9E9E) val dot = if (sk.state == 2) Color(0xFF81C784) else Color(0xFFAEBCC8)
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Box(Modifier.size(8.dp).clip(CircleShape).background(dot)) Box(Modifier.size(8.dp).clip(CircleShape).background(dot))
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
@ -338,14 +338,14 @@ private fun MemberDetailCard(
Spacer(Modifier.height(6.dp)) Spacer(Modifier.height(6.dp))
Text("Connections:", fontSize = 13.sp, fontWeight = FontWeight.Medium) Text("Connections:", fontSize = 13.sp, fontWeight = FontWeight.Medium)
memberLinks.links.forEachIndexed { idx, lk -> memberLinks.links.forEachIndexed { idx, lk ->
val dcol = if (lk.linkState == 3 && lk.linkStatus == 1) Color(0xFF4CAF50) else Color(0xFFE53935) val dcol = if (lk.linkState == 3 && lk.linkStatus == 1) Color(0xFF81C784) else Color(0xFFFFB4AB)
val arrow = if (lk.isServer) "◀" else "▶" val arrow = if (lk.isServer) "◀" else "▶"
val arrowColor = if (lk.isServer) Color(0xFFFF9800) else Color(0xFF2196F3) val arrowColor = if (lk.isServer) Color(0xFFFF9800) else Color(0xFF79B8FF)
Column { Column {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Box(Modifier.size(8.dp).clip(CircleShape).background(dcol)) Box(Modifier.size(8.dp).clip(CircleShape).background(dcol))
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
Text(lk.protocol, fontFamily = FontFamily.Monospace, fontSize = 12.sp, color = Color.Gray) Text(lk.protocol, fontFamily = FontFamily.Monospace, fontSize = 12.sp, color = Color(0xFFAEBCC8))
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
Text("${formatAddr(lk.localAddr)}:${lk.localPort}", Text("${formatAddr(lk.localAddr)}:${lk.localPort}",
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
@ -363,7 +363,7 @@ private fun MemberDetailCard(
if (cleanSn.isNotEmpty()) { if (cleanSn.isNotEmpty()) {
Row { Row {
Spacer(Modifier.width(14.dp)) Spacer(Modifier.width(14.dp))
Text(cleanSn, fontSize = 12.sp, color = Color.Gray) Text(cleanSn, fontSize = 12.sp, color = Color(0xFFAEBCC8))
} }
} }
} }
@ -376,7 +376,7 @@ private fun MemberDetailCard(
if (detail.directPresence) { if (detail.directPresence) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Box(Modifier.size(8.dp).clip(CircleShape) Box(Modifier.size(8.dp).clip(CircleShape)
.background(if (detail.directUp) Color(0xFF4CAF50) else Color(0xFFE53935))) .background(if (detail.directUp) Color(0xFF81C784) else Color(0xFFFFB4AB)))
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
Text("Direct: ${if (detail.directUp) "UP" else "DOWN"}" + Text("Direct: ${if (detail.directUp) "UP" else "DOWN"}" +
(if (detail.directUp && detail.bestRtt > 0) " ${detail.bestRtt / 10}ms" else ""), (if (detail.directUp && detail.bestRtt > 0) " ${detail.bestRtt / 10}ms" else ""),
@ -386,7 +386,7 @@ private fun MemberDetailCard(
if (detail.indirectPresence) { if (detail.indirectPresence) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Box(Modifier.size(8.dp).clip(CircleShape) Box(Modifier.size(8.dp).clip(CircleShape)
.background(if (detail.indirectUp) Color(0xFF4CAF50) else Color(0xFFE53935))) .background(if (detail.indirectUp) Color(0xFF81C784) else Color(0xFFFFB4AB)))
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
Text("Indirect: ${if (detail.indirectUp) "UP" else "DOWN"}", Text("Indirect: ${if (detail.indirectUp) "UP" else "DOWN"}",
fontSize = 13.sp, color = Color.DarkGray) fontSize = 13.sp, color = Color.DarkGray)
@ -394,7 +394,7 @@ private fun MemberDetailCard(
} }
if (detail.bgpPresence && detail.bgpNodes > 0) { if (detail.bgpPresence && detail.bgpNodes > 0) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Box(Modifier.size(8.dp).clip(CircleShape).background(Color(0xFF4CAF50))) Box(Modifier.size(8.dp).clip(CircleShape).background(Color(0xFF81C784)))
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
Text("BGP: ${detail.bgpNodes} nodes" + Text("BGP: ${detail.bgpNodes} nodes" +
(if (detail.bestRtt > 0) " ${detail.bestRtt / 10}ms" else ""), (if (detail.bestRtt > 0) " ${detail.bestRtt / 10}ms" else ""),
@ -402,7 +402,7 @@ private fun MemberDetailCard(
} }
} }
if (!detail.directPresence && !detail.indirectPresence && !detail.bgpPresence) { if (!detail.directPresence && !detail.indirectPresence && !detail.bgpPresence) {
Text("offline", fontSize = 13.sp, color = Color.Gray) Text("offline", fontSize = 13.sp, color = Color(0xFFAEBCC8))
} }
} }
} }
@ -478,7 +478,7 @@ private fun MemberEditDialog(
Text( Text(
"Admin/Super require channel private key", "Admin/Super require channel private key",
fontSize = 11.sp, fontSize = 11.sp,
color = Color.Gray color = Color(0xFFAEBCC8)
) )
} }
} }

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

@ -264,12 +264,12 @@ fun PhotoRecordScreen(
val selected = selectedLens?.cameraId == lens.cameraId val selected = selectedLens?.cameraId == lens.cameraId
Surface( Surface(
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
color = if (selected) Color(0xFFFFD600) else Color(0x66000000), color = if (selected) Color(0xFF214563) else Color(0xFF24323D),
modifier = Modifier.clickable { selectLens(lens) } modifier = Modifier.clickable { selectLens(lens) }
) { ) {
Text( Text(
lens.label, lens.label,
color = if (selected) Color.Black else Color.White, color = Color(0xFFE6EDF3),
fontSize = 13.sp, fontSize = 13.sp,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp) modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp)
) )
@ -282,12 +282,12 @@ fun PhotoRecordScreen(
val selected = r == selectedResolution val selected = r == selectedResolution
Surface( Surface(
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
color = if (selected) Color(0xFFFFD600) else Color(0x66000000), color = if (selected) Color(0xFF214563) else Color(0xFF24323D),
modifier = Modifier.clickable { selectedResolution = r } modifier = Modifier.clickable { selectedResolution = r }
) { ) {
Text( Text(
r.label, r.label,
color = if (selected) Color.Black else Color.White, color = Color(0xFFE6EDF3),
fontSize = 13.sp, fontSize = 13.sp,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
) )
@ -308,7 +308,7 @@ fun PhotoRecordScreen(
if (phase == PhotoPhase.CAPTURED) { if (phase == PhotoPhase.CAPTURED) {
Box( Box(
modifier = Modifier.size(52.dp) modifier = Modifier.size(52.dp)
.background(Color(0x66000000), CircleShape) .background(Color(0xCC000000), CircleShape)
.clickable { discardAndReset() }, .clickable { discardAndReset() },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
@ -317,7 +317,7 @@ fun PhotoRecordScreen(
Spacer(Modifier.width(28.dp)) Spacer(Modifier.width(28.dp))
Box( Box(
modifier = Modifier.size(52.dp) modifier = Modifier.size(52.dp)
.background(Color(0xFF4CAF50), CircleShape) .background(Color(0xFF24633D), CircleShape)
.clickable { .clickable {
outputFile?.let { f -> if (f.length() > 0) onSend(f.absolutePath, photoWidth, photoHeight) else onClose() } outputFile?.let { f -> if (f.length() > 0) onSend(f.absolutePath, photoWidth, photoHeight) else onClose() }
}, },

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

@ -248,7 +248,7 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
Text("No sockets detected", color = MaterialTheme.colorScheme.onSurfaceVariant) Text("No sockets detected", color = MaterialTheme.colorScheme.onSurfaceVariant)
} else { } else {
localSocketStates.forEach { s -> localSocketStates.forEach { s ->
val stateColor = when (s.state) { 2 -> Color(0xFF27AE60); 1 -> Color(0xFFF1C40F); else -> Color(0xFFE53935) } val stateColor = when (s.state) { 2 -> Color(0xFF81C784); 1 -> Color(0xFFF1C40F); else -> Color(0xFFFFB4AB) }
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) { Column(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text("${s.bindIp}:${s.port}", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall) Text("${s.bindIp}:${s.port}", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall)
@ -405,7 +405,7 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
Text("Service", style = MaterialTheme.typography.titleMedium) Text("Service", style = MaterialTheme.typography.titleMedium)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text(if (serviceEnabled) "Running" else "Stopped", modifier = Modifier.weight(1f), Text(if (serviceEnabled) "Running" else "Stopped", modifier = Modifier.weight(1f),
color = if (serviceEnabled) Color(0xFF27AE60) else MaterialTheme.colorScheme.error, color = if (serviceEnabled) Color(0xFF81C784) else MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodyMedium) style = MaterialTheme.typography.bodyMedium)
Button(onClick = { Button(onClick = {
runBlocking { provider.setServiceEnabled(true) } runBlocking { provider.setServiceEnabled(true) }
@ -798,7 +798,7 @@ private fun AudioSettingsTab(vm: ChatViewModel) {
LinearProgressIndicator( LinearProgressIndicator(
progress = { lvlPct }, progress = { lvlPct },
modifier = Modifier.weight(1f).height(16.dp), modifier = Modifier.weight(1f).height(16.dp),
color = when { lvlPct < 0.6f -> Color(0xFF27AE60); lvlPct < 0.85f -> Color(0xFFF1C40F); else -> Color(0xFFE53935) }, color = when { lvlPct < 0.6f -> Color(0xFF81C784); lvlPct < 0.85f -> Color(0xFFF1C40F); else -> Color(0xFFFFB4AB) },
trackColor = MaterialTheme.colorScheme.surfaceVariant, trackColor = MaterialTheme.colorScheme.surfaceVariant,
) )
Button(onClick = { Button(onClick = {

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

@ -387,7 +387,7 @@ fun VideoPlayerScreen(filePath: String, onClose: () -> Unit) {
if (error == null) { if (error == null) {
IconButton( IconButton(
onClick = { togglePlay() }, onClick = { togglePlay() },
modifier = Modifier.size(72.dp).background(Color(0x66000000), MaterialTheme.shapes.extraLarge) modifier = Modifier.size(72.dp).background(Color(0xCC000000), MaterialTheme.shapes.extraLarge)
) { ) {
Icon( Icon(
if (isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow, if (isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow,
@ -409,7 +409,7 @@ fun VideoPlayerScreen(filePath: String, onClose: () -> Unit) {
onClick = { flipLandscape() }, onClick = { flipLandscape() },
modifier = Modifier.padding(start = 12.dp) modifier = Modifier.padding(start = 12.dp)
.size(56.dp) .size(56.dp)
.background(Color(0x66000000), MaterialTheme.shapes.extraLarge) .background(Color(0xCC000000), MaterialTheme.shapes.extraLarge)
) { ) {
Icon( Icon(
Icons.Filled.ScreenRotation, Icons.Filled.ScreenRotation,
@ -427,7 +427,7 @@ fun VideoPlayerScreen(filePath: String, onClose: () -> Unit) {
onClick = { flipLandscape() }, onClick = { flipLandscape() },
modifier = Modifier.padding(end = 12.dp) modifier = Modifier.padding(end = 12.dp)
.size(56.dp) .size(56.dp)
.background(Color(0x66000000), MaterialTheme.shapes.extraLarge) .background(Color(0xCC000000), MaterialTheme.shapes.extraLarge)
) { ) {
Icon( Icon(
Icons.Filled.ScreenRotation, Icons.Filled.ScreenRotation,

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

@ -279,7 +279,7 @@ fun VideoRecordScreen(
val selected = selectedLens?.cameraId == lens.cameraId val selected = selectedLens?.cameraId == lens.cameraId
Surface( Surface(
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
color = if (selected) Color(0xFFFFD600) else Color(0x66000000), color = if (selected) Color(0xFF214563) else Color(0xFF24323D),
modifier = Modifier.clickable { modifier = Modifier.clickable {
selectedLens = lens selectedLens = lens
bindCamera(lens.selector()) bindCamera(lens.selector())
@ -291,13 +291,13 @@ fun VideoRecordScreen(
) { ) {
Text( Text(
lens.label, lens.label,
color = if (selected) Color.Black else Color.White, color = Color(0xFFE6EDF3),
fontSize = 13.sp fontSize = 13.sp
) )
if (lens.mm.isNotEmpty()) { if (lens.mm.isNotEmpty()) {
Text( Text(
lens.mm, lens.mm,
color = (if (selected) Color.Black else Color.White).copy(alpha = 0.7f), color = Color(0xFFAEBCC8),
fontSize = 11.sp fontSize = 11.sp
) )
} }
@ -311,12 +311,12 @@ fun VideoRecordScreen(
val selected = q == quality val selected = q == quality
Surface( Surface(
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
color = if (selected) Color(0xFFFFD600) else Color(0x66000000), color = if (selected) Color(0xFF214563) else Color(0xFF24323D),
modifier = Modifier.clickable { quality = q } modifier = Modifier.clickable { quality = q }
) { ) {
Text( Text(
q.label, q.label,
color = if (selected) Color.Black else Color.White, color = Color(0xFFE6EDF3),
fontSize = 13.sp, fontSize = 13.sp,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
) )
@ -346,7 +346,7 @@ fun VideoRecordScreen(
/* discard / close */ /* discard / close */
Box( Box(
modifier = Modifier.size(52.dp) modifier = Modifier.size(52.dp)
.background(Color(0x66000000), CircleShape) .background(Color(0xCC000000), CircleShape)
.clickable { if (phase == Phase.IDLE) onClose() else discardAndReset() }, .clickable { if (phase == Phase.IDLE) onClose() else discardAndReset() },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
@ -358,7 +358,7 @@ fun VideoRecordScreen(
Box( Box(
modifier = Modifier.size(76.dp) modifier = Modifier.size(76.dp)
.background( .background(
when (phase) { Phase.RECORDING -> Color(0xFFE53935); else -> Color(0xFFE53935) }, when (phase) { Phase.RECORDING -> Color(0xFFB32632); else -> Color(0xFFB32632) },
CircleShape CircleShape
) )
.clickable { .clickable {
@ -389,7 +389,7 @@ fun VideoRecordScreen(
val canSend = (phase == Phase.RECORDING || phase == Phase.PAUSED || phase == Phase.STOPPED) val canSend = (phase == Phase.RECORDING || phase == Phase.PAUSED || phase == Phase.STOPPED)
Box( Box(
modifier = Modifier.size(52.dp) modifier = Modifier.size(52.dp)
.background(if (canSend) Color(0xFF4CAF50) else Color(0x33000000), CircleShape) .background(if (canSend) Color(0xFF24633D) else Color(0x33000000), CircleShape)
.clickable(enabled = canSend) { .clickable(enabled = canSend) {
if (phase == Phase.STOPPED) { if (phase == Phase.STOPPED) {
outputFile?.let { onSend(it.absolutePath) } outputFile?.let { onSend(it.absolutePath) }

50
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/theme/Theme.kt

@ -0,0 +1,50 @@
package com.utun.chat.ui.theme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.utun.chat.data.LogManager
// Общая палитра экранов, диалогов и сообщений; подписи не ослабляем прозрачностью.
object ChatColors {
val Background = Color(0xFF0F1B22)
val Panel = Color(0xFF1B2733)
val Raised = Color(0xFF24323D)
val Outgoing = Color(0xFF214563)
val Text = Color(0xFFE6EDF3)
val Secondary = Color(0xFFAEBCC8)
val Accent = Color(0xFF79B8FF)
val Success = Color(0xFF81C784)
val Error = Color(0xFFFFB4AB)
}
private val DarkColors = darkColorScheme(
primary = ChatColors.Accent, onPrimary = ChatColors.Background,
primaryContainer = ChatColors.Outgoing, onPrimaryContainer = ChatColors.Text,
secondary = ChatColors.Accent, onSecondary = ChatColors.Background,
secondaryContainer = ChatColors.Raised, onSecondaryContainer = ChatColors.Text,
tertiary = ChatColors.Success, onTertiary = ChatColors.Background,
tertiaryContainer = Color(0xFF203C2C), onTertiaryContainer = ChatColors.Text,
background = ChatColors.Background, onBackground = ChatColors.Text,
surface = ChatColors.Panel, onSurface = ChatColors.Text,
surfaceVariant = ChatColors.Raised, onSurfaceVariant = ChatColors.Secondary,
surfaceTint = Color.Transparent,
inverseSurface = ChatColors.Raised, inverseOnSurface = ChatColors.Text, inversePrimary = ChatColors.Accent,
outline = Color(0xFF8293A1), outlineVariant = Color(0xFF435563),
error = ChatColors.Error, onError = Color(0xFF4A1518),
errorContainer = Color(0xFF54252A), onErrorContainer = ChatColors.Text,
scrim = Color.Black
)
@Composable
fun VibeChatTheme(content: @Composable () -> Unit) {
LaunchedEffect(Unit) { LogManager.addLog("INFO", "UI", "Dark theme active: screens, dialogs and message palette") }
MaterialTheme(colorScheme = DarkColors) {
Surface(modifier = Modifier.fillMaxSize(), color = ChatColors.Background, content = content)
}
}

10
tools/chatgui-android/app/src/main/res/values/themes.xml

@ -1,4 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.UTun" parent="android:Theme.Material.Light.NoActionBar" /> <style name="Theme.UTun" parent="android:Theme.Material.NoActionBar">
<item name="android:windowBackground">#0F1B22</item>
<item name="android:colorBackground">#0F1B22</item>
<item name="android:statusBarColor">#0F1B22</item>
<item name="android:navigationBarColor">#0F1B22</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar">false</item>
<item name="android:colorAccent">#79B8FF</item>
</style>
</resources> </resources>

Loading…
Cancel
Save