diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt index 3a8d5aac..7161d1e2 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt +++ b/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.RadioAudioEngine 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.screens.* import com.utun.chat.viewmodel.ChatViewModel @@ -58,10 +59,10 @@ class MainActivity : ComponentActivity() { showOverLockScreen() } - /* Edge-to-edge with dark status/navigation bar icons (light app theme) */ + /* Светлые системные значки на тёмном фоне приложения. */ enableEdgeToEdge( - statusBarStyle = SystemBarStyle.light(android.graphics.Color.TRANSPARENT, android.graphics.Color.TRANSPARENT), - navigationBarStyle = SystemBarStyle.light(android.graphics.Color.TRANSPARENT, android.graphics.Color.TRANSPARENT) + statusBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT), + navigationBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT) ) /* Start uTun headless service (must be from foreground Activity on API 34+). @@ -71,288 +72,290 @@ class MainActivity : ComponentActivity() { } setContent { - val vm: ChatViewModel = viewModel(factory = ChatViewModel.Factory()) - - /* Pause GUI polling (health-check, recording/playback timers) when backgrounded, - refresh statuses on resume. */ - LifecycleStartEffect(Unit) { - vm.setForeground(true) - onStopOrDispose { vm.setForeground(false) } - } - - var screen by remember { mutableStateOf(Screen.ChannelList) } - var qrResult by remember { mutableStateOf(null) } - var showJoinDialog by remember { mutableStateOf(false) } - var joinAutoLink by remember { mutableStateOf(null) } - var inviteChannelId by remember { mutableStateOf(null) } - var videoMessage by remember { mutableStateOf(null) } - var photoMessage by remember { mutableStateOf(null) } - var hadCall by remember { mutableStateOf(false) } - var screenBeforeCall by remember { mutableStateOf(null) } - val snackbarHostState = remember { SnackbarHostState() } - val scope = rememberCoroutineScope() - - val callState by vm.callState.collectAsState() - - /* Пока идёт звонок — держим экран включённым; после завершения снимаем - и showWhenLocked (если он был выставлен для входящего поверх блокировки). */ - LaunchedEffect(callState) { - val st = callState - val inCall = st != null && st.phase != ChatViewModel.CallPhase.NONE && - st.phase != ChatViewModel.CallPhase.ENDED - if (inCall) { - hadCall = true - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } else { - window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - if (hadCall) releaseLockScreen() + VibeChatTheme { + val vm: ChatViewModel = viewModel(factory = ChatViewModel.Factory()) + + /* Pause GUI polling (health-check, recording/playback timers) when backgrounded, + refresh statuses on resume. */ + LifecycleStartEffect(Unit) { + vm.setForeground(true) + onStopOrDispose { vm.setForeground(false) } } - } - /* Звонок управляет навигацией: OUTGOING/RINGING/ACTIVE → экран Call (запоминаем - откуда пришли, чтобы свернуться обратно). ENDED не навигирует — CallScreen сам - показывает «Завершён», если пользователь уже на нём. INCOMING → диалог поверх. */ - LaunchedEffect(callState?.phase) { - when (callState?.phase) { - ChatViewModel.CallPhase.OUTGOING, ChatViewModel.CallPhase.RINGING, - ChatViewModel.CallPhase.ACTIVE -> { - if (screen != Screen.Call) screenBeforeCall = screen - screen = Screen.Call + var screen by remember { mutableStateOf(Screen.ChannelList) } + var qrResult by remember { mutableStateOf(null) } + var showJoinDialog by remember { mutableStateOf(false) } + var joinAutoLink by remember { mutableStateOf(null) } + var inviteChannelId by remember { mutableStateOf(null) } + var videoMessage by remember { mutableStateOf(null) } + var photoMessage by remember { mutableStateOf(null) } + var hadCall by remember { mutableStateOf(false) } + var screenBeforeCall by remember { mutableStateOf(null) } + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + + val callState by vm.callState.collectAsState() + + /* Пока идёт звонок — держим экран включённым; после завершения снимаем + и showWhenLocked (если он был выставлен для входящего поверх блокировки). */ + LaunchedEffect(callState) { + val st = callState + val inCall = st != null && st.phase != ChatViewModel.CallPhase.NONE && + st.phase != ChatViewModel.CallPhase.ENDED + if (inCall) { + hadCall = true + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + if (hadCall) releaseLockScreen() } - null -> if (screen == Screen.Call) screen = screenBeforeCall ?: Screen.ChannelList - else -> {} } - } - /* Автооткрытие DM-беседы после нажатия «Message» в списке участников. */ - LaunchedEffect(Unit) { - vm.dmOpen.collect { convId -> - if (convId != null) { - vm.consumeDmOpen() - screen = Screen.Dm + /* Звонок управляет навигацией: OUTGOING/RINGING/ACTIVE → экран Call (запоминаем + откуда пришли, чтобы свернуться обратно). ENDED не навигирует — CallScreen сам + показывает «Завершён», если пользователь уже на нём. INCOMING → диалог поверх. */ + LaunchedEffect(callState?.phase) { + when (callState?.phase) { + ChatViewModel.CallPhase.OUTGOING, ChatViewModel.CallPhase.RINGING, + ChatViewModel.CallPhase.ACTIVE -> { + if (screen != Screen.Call) screenBeforeCall = screen + screen = Screen.Call + } + null -> if (screen == Screen.Call) screen = screenBeforeCall ?: Screen.ChannelList + else -> {} } } - } - val showInvite: (Channel) -> Unit = { ch -> - if (ch.id.toULongOrNull() == null) { - LogManager.addLog("ERROR", "INVITE", "channel id is not numeric: ${ch.id}") - scope.launch { snackbarHostState.showSnackbar("Invalid channel id") } - } else { - inviteChannelId = ch.id - vm.requestInvite(ch.id) + /* Автооткрытие DM-беседы после нажатия «Message» в списке участников. */ + LaunchedEffect(Unit) { + vm.dmOpen.collect { convId -> + if (convId != null) { + vm.consumeDmOpen() + screen = Screen.Dm + } + } } - } - BackHandler(enabled = screen != Screen.ChannelList) { - when (screen) { - Screen.MemberList -> screen = Screen.Chat - Screen.VideoRecord -> screen = Screen.Chat - Screen.PhotoRecord -> screen = Screen.Chat - Screen.Chat -> screen = Screen.ChannelList - Screen.Dm -> { vm.closeDm(); screen = Screen.ChannelList } - Screen.Call -> screen = screenBeforeCall ?: Screen.ChannelList - else -> screen = Screen.ChannelList + val showInvite: (Channel) -> Unit = { ch -> + if (ch.id.toULongOrNull() == null) { + LogManager.addLog("ERROR", "INVITE", "channel id is not numeric: ${ch.id}") + scope.launch { snackbarHostState.showSnackbar("Invalid channel id") } + } else { + inviteChannelId = ch.id + vm.requestInvite(ch.id) + } } - } - BackHandler(enabled = videoMessage != null) { - videoMessage = null - } + BackHandler(enabled = screen != Screen.ChannelList) { + when (screen) { + Screen.MemberList -> screen = Screen.Chat + Screen.VideoRecord -> screen = Screen.Chat + Screen.PhotoRecord -> screen = Screen.Chat + Screen.Chat -> screen = Screen.ChannelList + Screen.Dm -> { vm.closeDm(); screen = Screen.ChannelList } + Screen.Call -> screen = screenBeforeCall ?: Screen.ChannelList + else -> screen = Screen.ChannelList + } + } - BackHandler(enabled = photoMessage != null) { - photoMessage = null - } + BackHandler(enabled = videoMessage != null) { + videoMessage = null + } - val connected = NativeLib.isReady() + BackHandler(enabled = photoMessage != null) { + photoMessage = null + } - /* Subscribe native logs only when LogScreen is open */ - DisposableEffect(screen) { - if (screen == Screen.Logs) NativeLib.subscribeNativeLogs(true) - onDispose { NativeLib.subscribeNativeLogs(false) } - } + val connected = NativeLib.isReady() - Column(modifier = Modifier.fillMaxSize()) { - val callPhase = callState?.phase - val showCallBanner = screen != Screen.Call && - (callPhase == ChatViewModel.CallPhase.OUTGOING || - callPhase == ChatViewModel.CallPhase.RINGING || - callPhase == ChatViewModel.CallPhase.ACTIVE) - if (showCallBanner) { - ActiveCallBanner( - viewModel = vm, - onOpen = { screen = Screen.Call }, - onHangup = { vm.hangupCall() } - ) + /* Subscribe native logs only when LogScreen is open */ + DisposableEffect(screen) { + if (screen == Screen.Logs) NativeLib.subscribeNativeLogs(true) + onDispose { NativeLib.subscribeNativeLogs(false) } } - Box( - modifier = Modifier.weight(1f) - .then(if (showCallBanner) Modifier.consumeWindowInsets(WindowInsets.statusBars) else Modifier) - ) { - when (screen) { - Screen.ChannelList -> { - val channels by vm.channels.collectAsState() - val dmConversations by vm.dmConversations.collectAsState() - val stuck by vm.isStuck.collectAsState() - ChannelListScreen( - channels = channels, - dmConversations = dmConversations, - connected = connected, - isStuck = stuck, - onSelectChannel = { ch -> vm.selectChannel(ch); screen = Screen.Chat }, - onSelectDm = { dm -> vm.openDm(dm.convId, dm.peerNodeId, dm.peerName); screen = Screen.Dm }, - onCreateChannel = { name -> vm.createChannel(name, "channel_1") }, - onJoinChannel = { showJoinDialog = true; joinAutoLink = null }, - onInviteClick = showInvite, - onDeleteChannel = { vm.deleteChannel(it.id) }, - onSettings = { screen = Screen.Settings }, - onLogs = { screen = Screen.Logs }, - onRefresh = { vm.requestRefresh() } - ) - } - Screen.Chat -> { - val messages by vm.messages.collectAsState() - val channel = vm.currentChannel.collectAsState().value - if (channel != null) { - ChatScreen( - channel = channel, - messages = messages, + + Column(modifier = Modifier.fillMaxSize()) { + val callPhase = callState?.phase + val showCallBanner = screen != Screen.Call && + (callPhase == ChatViewModel.CallPhase.OUTGOING || + callPhase == ChatViewModel.CallPhase.RINGING || + callPhase == ChatViewModel.CallPhase.ACTIVE) + if (showCallBanner) { + ActiveCallBanner( + viewModel = vm, + onOpen = { screen = Screen.Call }, + onHangup = { vm.hangupCall() } + ) + } + Box( + modifier = Modifier.weight(1f) + .then(if (showCallBanner) Modifier.consumeWindowInsets(WindowInsets.statusBars) else Modifier) + ) { + when (screen) { + Screen.ChannelList -> { + val channels by vm.channels.collectAsState() + val dmConversations by vm.dmConversations.collectAsState() + val stuck by vm.isStuck.collectAsState() + ChannelListScreen( + channels = channels, + dmConversations = dmConversations, connected = connected, - viewModel = vm, - onSendMessage = { vm.sendMessage(it) }, - onMembersClick = { screen = Screen.MemberList }, - onInvite = { showInvite(channel) }, - onBack = { screen = Screen.ChannelList }, - onPlayVideo = { msg -> videoMessage = msg }, - onViewPhoto = { msg -> photoMessage = msg }, - onRecordVideo = { screen = Screen.VideoRecord }, - onCapturePhoto = { screen = Screen.PhotoRecord } + isStuck = stuck, + onSelectChannel = { ch -> vm.selectChannel(ch); screen = Screen.Chat }, + onSelectDm = { dm -> vm.openDm(dm.convId, dm.peerNodeId, dm.peerName); screen = Screen.Dm }, + onCreateChannel = { name -> vm.createChannel(name, "channel_1") }, + onJoinChannel = { showJoinDialog = true; joinAutoLink = null }, + onInviteClick = showInvite, + onDeleteChannel = { vm.deleteChannel(it.id) }, + onSettings = { screen = Screen.Settings }, + onLogs = { screen = Screen.Logs }, + onRefresh = { vm.requestRefresh() } ) } - } - Screen.MemberList -> { - val members by vm.members.collectAsState() - val selectedMember by vm.selectedMember.collectAsState() - val channel = vm.currentChannel.collectAsState().value - if (channel != null) { - MemberListScreen( - channel = channel, - members = members, - selectedMember = selectedMember, - viewModel = vm, - onBack = { screen = Screen.Chat } - ) + Screen.Chat -> { + val messages by vm.messages.collectAsState() + val channel = vm.currentChannel.collectAsState().value + if (channel != null) { + ChatScreen( + channel = channel, + messages = messages, + connected = connected, + viewModel = vm, + onSendMessage = { vm.sendMessage(it) }, + onMembersClick = { screen = Screen.MemberList }, + onInvite = { showInvite(channel) }, + onBack = { screen = Screen.ChannelList }, + onPlayVideo = { msg -> videoMessage = msg }, + onViewPhoto = { msg -> photoMessage = msg }, + onRecordVideo = { screen = Screen.VideoRecord }, + onCapturePhoto = { screen = Screen.PhotoRecord } + ) + } } - } - Screen.Dm -> { - val dmMessages by vm.dmMessages.collectAsState() - val dm = vm.currentDm.collectAsState().value - if (dm != null) { - DmChatScreen( - conv = dm, - messages = dmMessages, - onSendMessage = { vm.sendDmMessage(it) }, - onBack = { vm.closeDm(); screen = Screen.ChannelList } - ) + Screen.MemberList -> { + val members by vm.members.collectAsState() + val selectedMember by vm.selectedMember.collectAsState() + val channel = vm.currentChannel.collectAsState().value + if (channel != null) { + MemberListScreen( + channel = channel, + members = members, + selectedMember = selectedMember, + viewModel = vm, + onBack = { screen = Screen.Chat } + ) + } } - } - Screen.Settings -> SettingsScreen(vm = vm, onBack = { screen = Screen.ChannelList }, onExit = { finish() }) - Screen.Logs -> LogScreen(onBack = { screen = Screen.ChannelList }) - Screen.Call -> CallScreen( - viewModel = vm, - onClose = { vm.dismissCall(); screen = screenBeforeCall ?: Screen.ChannelList }, - onMinimize = { screen = screenBeforeCall ?: Screen.ChannelList } - ) - Screen.QrScan -> { - QrScanScreen( - onResult = { link -> - qrResult = link - screen = Screen.ChannelList - }, - onBack = { screen = Screen.ChannelList } + Screen.Dm -> { + val dmMessages by vm.dmMessages.collectAsState() + val dm = vm.currentDm.collectAsState().value + if (dm != null) { + DmChatScreen( + conv = dm, + messages = dmMessages, + onSendMessage = { vm.sendDmMessage(it) }, + onBack = { vm.closeDm(); screen = Screen.ChannelList } + ) + } + } + Screen.Settings -> SettingsScreen(vm = vm, onBack = { screen = Screen.ChannelList }, onExit = { finish() }) + Screen.Logs -> LogScreen(onBack = { screen = Screen.ChannelList }) + Screen.Call -> CallScreen( + viewModel = vm, + onClose = { vm.dismissCall(); screen = screenBeforeCall ?: Screen.ChannelList }, + onMinimize = { screen = screenBeforeCall ?: Screen.ChannelList } ) - } - Screen.VideoRecord -> { - val channel = vm.currentChannel.collectAsState().value - if (channel != null) { - VideoRecordScreen( - onSend = { path -> - vm.sendVideoMessage(path) - screen = Screen.Chat + Screen.QrScan -> { + QrScanScreen( + onResult = { link -> + qrResult = link + screen = Screen.ChannelList }, - onClose = { screen = Screen.Chat } + onBack = { screen = Screen.ChannelList } ) } + Screen.VideoRecord -> { + val channel = vm.currentChannel.collectAsState().value + if (channel != null) { + VideoRecordScreen( + onSend = { path -> + vm.sendVideoMessage(path) + screen = Screen.Chat + }, + onClose = { screen = Screen.Chat } + ) + } + } + Screen.PhotoRecord -> { + val channel = vm.currentChannel.collectAsState().value + if (channel != null) { + PhotoRecordScreen( + onSend = { path, width, height -> + vm.sendPhotoMessage(path, width, height) + screen = Screen.Chat + }, + onClose = { screen = Screen.Chat } + ) + } + } } - Screen.PhotoRecord -> { - val channel = vm.currentChannel.collectAsState().value - if (channel != null) { - PhotoRecordScreen( - onSend = { path, width, height -> - vm.sendPhotoMessage(path, width, height) - screen = Screen.Chat - }, - onClose = { screen = Screen.Chat } - ) + SnackbarHost(snackbarHostState, Modifier.align(Alignment.BottomCenter).navigationBarsPadding()) + + videoMessage?.let { msg -> + if (msg.filePath.isNotEmpty()) { + VideoPlayerScreen(filePath = msg.filePath, onClose = { videoMessage = null }) } } - } - SnackbarHost(snackbarHostState, Modifier.align(Alignment.BottomCenter).navigationBarsPadding()) - videoMessage?.let { msg -> - if (msg.filePath.isNotEmpty()) { - VideoPlayerScreen(filePath = msg.filePath, onClose = { videoMessage = null }) + photoMessage?.let { msg -> + if (msg.filePath.isNotEmpty()) { + PhotoViewerScreen(filePath = msg.filePath, onClose = { photoMessage = null }) + } } - } - photoMessage?.let { msg -> - if (msg.filePath.isNotEmpty()) { - PhotoViewerScreen(filePath = msg.filePath, onClose = { photoMessage = null }) + if (callState?.phase == ChatViewModel.CallPhase.INCOMING) { + IncomingCallDialog(viewModel = vm) } } + } - if (callState?.phase == ChatViewModel.CallPhase.INCOMING) { - IncomingCallDialog(viewModel = vm) + /* Show join dialog after QR scan returns */ + LaunchedEffect(qrResult) { + qrResult?.let { link -> + joinAutoLink = link + showJoinDialog = true + qrResult = null } } - } - /* Show join dialog after QR scan returns */ - LaunchedEffect(qrResult) { - qrResult?.let { link -> - joinAutoLink = link - showJoinDialog = true - qrResult = null + if (showJoinDialog) { + JoinChannelDialog( + autoLink = joinAutoLink, + onScanQr = { + screen = Screen.QrScan + showJoinDialog = false + }, + onConnect = { data: InviteData -> + val addrs = data.serializeAddrs() + NativeLib.joinChannel(data.channelId, data.nodeId, data.joinKey, data.pubkey, addrs, data.addrs.size) + showJoinDialog = false + }, + onDismiss = { showJoinDialog = false } + ) } - } - - if (showJoinDialog) { - JoinChannelDialog( - autoLink = joinAutoLink, - onScanQr = { - screen = Screen.QrScan - showJoinDialog = false - }, - onConnect = { data: InviteData -> - val addrs = data.serializeAddrs() - NativeLib.joinChannel(data.channelId, data.nodeId, data.joinKey, data.pubkey, addrs, data.addrs.size) - showJoinDialog = false - }, - onDismiss = { showJoinDialog = false } - ) - } - inviteChannelId?.let { chId -> - val candidates by vm.inviteCandidates.collectAsState() - val link by vm.inviteLink.collectAsState() - val pending by vm.inviteLinkPending.collectAsState() - InviteLinkDialog( - candidates = candidates, - link = link, - pending = pending, - onSelectNode = { nodeId -> vm.requestInviteLink(chId, nodeId) }, - onDismiss = { inviteChannelId = null; vm.clearInvite() } - ) + inviteChannelId?.let { chId -> + val candidates by vm.inviteCandidates.collectAsState() + val link by vm.inviteLink.collectAsState() + val pending by vm.inviteLinkPending.collectAsState() + InviteLinkDialog( + candidates = candidates, + link = link, + pending = pending, + onSelectNode = { nodeId -> vm.requestInviteLink(chId, nodeId) }, + onDismiss = { inviteChannelId = null; vm.clearInvite() } + ) + } } } } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ActiveCallBanner.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ActiveCallBanner.kt index db7b3568..324b21ec 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ActiveCallBanner.kt +++ b/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)) Box( - modifier = Modifier.size(40.dp).background(Color(0xFFE53935), CircleShape), + modifier = Modifier.size(40.dp).background(Color(0xFFB32632), CircleShape), contentAlignment = Alignment.Center ) { IconButton(onClick = onHangup) { diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt index 8b165844..7ebc3d97 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt +++ b/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.geometry.Offset import androidx.compose.ui.geometry.Size +import com.utun.chat.ui.theme.ChatColors import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.input.pointer.pointerInput @@ -82,8 +83,8 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState = onOpenFile: (Message) -> Unit = {}, onOpenImage: (Message) -> Unit = {}, onPlayVideo: (Message) -> Unit = {}) { - val bgColor = if (message.isOutgoing) Color(0xFF4A90D9) else Color(0xFFE8E8E8) - val textColor = if (message.isOutgoing) Color.White else Color.Black + val bgColor = if (message.isOutgoing) ChatColors.Outgoing else ChatColors.Raised + val textColor = ChatColors.Text val alignment = if (message.isOutgoing) Arrangement.End else Arrangement.Start val minBubbleWidth = (LocalConfiguration.current.screenWidthDp / 3).dp val voiceMaxWidth = (LocalConfiguration.current.screenWidthDp * 0.75f).dp @@ -120,7 +121,7 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState = .background(bgColor) .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)) when { @@ -138,7 +139,7 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState = Text( formatTime(message.ts), fontSize = 11.sp, - color = textColor.copy(alpha = 0.5f), + color = ChatColors.Secondary, modifier = Modifier.align(Alignment.End) ) } @@ -164,7 +165,7 @@ private fun VoiceBubble(message: Message, textColor: Color, isActive: Boolean, Column { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Box( - modifier = Modifier.size(32.dp).background(Color.White, CircleShape) + modifier = Modifier.size(32.dp).background(ChatColors.Background, CircleShape) .clickable { when { downloading -> {} @@ -181,7 +182,7 @@ private fun VoiceBubble(message: Message, textColor: Color, isActive: Boolean, playing && !paused -> "\u23F8" else -> "\u25B6" }, - fontSize = 14.sp, color = Color.Black + fontSize = 14.sp, color = ChatColors.Text ) } Spacer(Modifier.width(8.dp)) @@ -212,7 +213,7 @@ private fun VoiceBubble(message: Message, textColor: Color, isActive: Boolean, val y = ph - barH val played = showCursor && i < playedIdx 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), size = Size(barW, barH) ) @@ -241,12 +242,12 @@ private fun VoiceBubble(message: Message, textColor: Color, isActive: Boolean, else -> formatDurationMs(durMs) }, fontSize = 12.sp, - color = textColor.copy(alpha = 0.55f) + color = ChatColors.Secondary ) Text( formatTime(message.ts), fontSize = 11.sp, - color = textColor.copy(alpha = 0.4f), + color = ChatColors.Secondary, modifier = Modifier.align(Alignment.CenterVertically).padding(start = 8.dp) ) } @@ -287,7 +288,7 @@ private fun FileBubble(message: Message, textColor: Color, onDownload: (Message) Text( "${formatFileSize(message.fileSize)} · $stateText", 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()) } 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( when { downloading -> "\u23F3"; isError -> "\u267B"; !downloaded -> "\u2B07"; else -> "\u25B6" }, fontSize = 18.sp, color = Color.White @@ -368,7 +369,7 @@ private fun VideoBubble(message: Message, textColor: Color, Text( formatDurationMs(durMs), fontSize = 11.sp, color = Color.White, 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) ) } @@ -392,7 +393,7 @@ private fun VideoBubble(message: Message, textColor: Color, else -> "Tap to play" }) }.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()) } 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( when { downloading -> "\u23F3"; isError -> "\u267B"; !downloaded -> "\u2B07"; else -> "" }, fontSize = 18.sp, color = Color.White @@ -453,7 +454,7 @@ private fun ImageBubble(message: Message, textColor: Color, !downloaded -> "Tap to download" 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( formatFileSize(pendingAttachmentSize), fontSize = 12.sp, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.6f) + color = ChatColors.Secondary ) Spacer(Modifier.width(8.dp)) } @@ -577,7 +578,7 @@ fun InputBar( Surface( modifier = Modifier.weight(1f).height(48.dp), shape = RoundedCornerShape(8.dp), - color = Color(0xFFE53935) + color = Color(0xFF54252A) ) { Row( modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp), @@ -615,10 +616,10 @@ fun InputBar( if (isRecording && isRecordingLocked) { /* Locked: cancel + send voice */ IconButton(onClick = onVoiceCancel) { - Text("\u2715", fontSize = 20.sp, color = Color(0xFFE53935)) + Text("\u2715", fontSize = 20.sp, color = Color(0xFFFFB4AB)) } 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)) { /* Send button — round, primary color */ @@ -630,11 +631,11 @@ fun InputBar( .clickable { onSend() }, contentAlignment = Alignment.Center ) { - Text("\u27A4", fontSize = 22.sp, color = Color.White) + Text("\u27A4", fontSize = 22.sp, color = MaterialTheme.colorScheme.onPrimary) } } else { /* 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( modifier = Modifier .size(48.dp) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallScreen.kt index a31b0c2f..1b122b18 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallScreen.kt +++ b/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)) - 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)) Text( if (phase == CallPhase.ACTIVE) fmtDuration(duration) else callStatusText(phase), - color = Color(0xFF9BA6AD), + color = Color(0xFFAEBCC8), fontSize = 16.sp ) val bgpPath = st?.bgpPath ?: "" @@ -176,7 +176,7 @@ fun CallScreen( Spacer(Modifier.height(8.dp)) Text( bgpPath, - color = Color(0xFF9BA6AD), + color = Color(0xFFAEBCC8), fontSize = 13.sp ) } @@ -231,13 +231,13 @@ fun CallScreen( contentDescription = if (muted) "Включить микрофон" else "Выключить микрофон", label = if (muted) "Вкл. микрофон" else "Mute", active = muted, - activeColor = Color(0xFFE53935), + activeColor = Color(0xFFB32632), onClick = { viewModel.toggleMute() } ) } Column(horizontalAlignment = Alignment.CenterHorizontally) { Box( - modifier = Modifier.size(72.dp).background(Color(0xFFE53935), CircleShape), + modifier = Modifier.size(72.dp).background(Color(0xFFB32632), CircleShape), contentAlignment = Alignment.Center ) { IconButton(onClick = { viewModel.hangupCall() }) { @@ -245,7 +245,7 @@ fun CallScreen( } } 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)) Text("Экран заблокирован", color = Color.White, fontSize = 20.sp, fontWeight = FontWeight.Medium) Spacer(Modifier.height(8.dp)) - Text("Отведите телефон от уха, чтобы разблокировать", color = Color(0xFF9BA6AD), fontSize = 14.sp) + Text("Отведите телефон от уха, чтобы разблокировать", color = Color(0xFFAEBCC8), fontSize = 14.sp) } } } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt index bff60c9d..b97ae41e 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt @@ -59,7 +59,7 @@ fun ChannelListScreen( actions = { Box( 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)) IconButton(onClick = onJoinChannel) { Icon(Icons.Default.QrCode, "Join") } @@ -86,7 +86,7 @@ fun ChannelListScreen( } else { LazyColumn { 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 { ch.peersOnline > 99 -> "99+" ch.peersOnline > 0 -> ch.peersOnline.toString() @@ -126,7 +126,7 @@ fun ChannelListScreen( .offset(x = (-4).dp, y = (-6).dp) .size(badgeSize) .clip(CircleShape) - .background(Color(0xFF4CAF50)), + .background(Color(0xFF24633D)), contentAlignment = Alignment.Center ) { Text( @@ -164,10 +164,10 @@ fun ChannelListScreen( items(dmConversations) { dm -> Surface(modifier = Modifier.clickable { onSelectDm(dm) }) { 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)) { 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)) Column { diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt index 7cf74c01..53b77310 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt @@ -366,7 +366,7 @@ private fun RadioPttButton( } }, shape = CircleShape, - color = if (isTalking) Color(0xFFE53935) else Color(0xFF81C784), + color = if (isTalking) Color(0xFFFFB4AB) else Color(0xFF81C784), shadowElevation = 6.dp ) { Box(contentAlignment = Alignment.Center) { @@ -459,7 +459,7 @@ private fun RadioControlButton( onCheckedChange = { onClick() }, modifier = Modifier.size(48.dp), colors = IconButtonDefaults.iconToggleButtonColors( - containerColor = Color(0xFF24323D), contentColor = Color(0xFF9BA6AD), + containerColor = Color(0xFF24323D), contentColor = Color(0xFFAEBCC8), checkedContainerColor = Color(0xFF1F6FEB), checkedContentColor = Color.White) ) { if (floating) { diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/IncomingCallDialog.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/IncomingCallDialog.kt index c601accb..796e7559 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/IncomingCallDialog.kt +++ b/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)) Text(name, color = Color.White, fontSize = 22.sp, fontWeight = FontWeight.Medium) Spacer(Modifier.height(6.dp)) - Text("Входящий звонок…", color = Color(0xFF9BA6AD), fontSize = 14.sp) + Text("Входящий звонок…", color = Color(0xFFAEBCC8), fontSize = 14.sp) Spacer(Modifier.height(28.dp)) Row(horizontalArrangement = Arrangement.spacedBy(48.dp)) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Box( - modifier = Modifier.size(64.dp).background(Color(0xFFE53935), CircleShape), + modifier = Modifier.size(64.dp).background(Color(0xFFB32632), CircleShape), contentAlignment = Alignment.Center ) { IconButton(onClick = { viewModel.declineCall() }) { 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) { Box( @@ -73,7 +73,7 @@ fun IncomingCallDialog( 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) } } } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/LogScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/LogScreen.kt index 54c8e89b..73943f11 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/LogScreen.kt +++ b/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 Text( label, - color = if (selected) Color.White else MaterialTheme.colorScheme.onSurfaceVariant, + color = if (selected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 11.sp, modifier = Modifier .clickable { filterLevel = lvl } @@ -77,12 +77,12 @@ fun LogScreen(onBack: () -> Unit) { items(filtered.size) { idx -> // index-based to avoid recomposition on each new entry val entry = filtered[idx] val color = when (entry.level) { - "ERROR" -> Color(0xFFFF4444) + "ERROR" -> Color(0xFFFFB4AB) "WARN" -> Color(0xFFFFAA00) - "INFO" -> Color(0xFF4CAF50) - "DEBUG" -> Color(0xFF2196F3) - "TRACE" -> Color(0xFF9E9E9E) - else -> Color.Gray + "INFO" -> Color(0xFF81C784) + "DEBUG" -> Color(0xFF79B8FF) + "TRACE" -> Color(0xFFAEBCC8) + else -> Color(0xFFAEBCC8) } Text( text = "${entry.level} [${entry.category}] ${entry.message}", 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 e90e2558..7d1f8185 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 @@ -143,8 +143,8 @@ private fun MemberListItem( ) { val onlineColor = when { member.connecting -> Color(0xFFFFEB3B) // yellow: connecting - member.online -> Color(0xFF4CAF50) // green: reachable (direct/indirect/relay) - else -> Color(0xFF9E9E9E) // gray: offline + member.online -> Color(0xFF81C784) // green: reachable (direct/indirect/relay) + else -> Color(0xFFAEBCC8) // gray: offline } val bgColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else Color.Transparent @@ -160,7 +160,7 @@ private fun MemberListItem( verticalAlignment = Alignment.CenterVertically ) { if (member.isSelf) { - Text("✓", fontSize = 12.sp, color = Color(0xFF4CAF50)) + Text("✓", fontSize = 12.sp, color = Color(0xFF81C784)) } else { Box( modifier = Modifier @@ -182,7 +182,7 @@ private fun MemberListItem( Spacer(Modifier.width(4.dp)) } 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)) } Text( @@ -192,7 +192,7 @@ private fun MemberListItem( maxLines = 1, overflow = TextOverflow.Ellipsis, 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) ) } @@ -215,7 +215,7 @@ private fun MemberListItem( text = parts.joinToString(" "), fontSize = 11.sp, 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())) { 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( modifier = Modifier .size(10.dp) @@ -271,7 +271,7 @@ private fun MemberDetailCard( Text(detail.name, fontWeight = FontWeight.Bold, fontSize = 15.sp) if (isSelf) { 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(" · "), fontFamily = FontFamily.Monospace, fontSize = 11.sp, - color = Color.Gray + color = Color(0xFFAEBCC8) ) if (isSelf) { @@ -307,7 +307,7 @@ private fun MemberDetailCard( Spacer(Modifier.height(6.dp)) Text("Listen sockets:", fontSize = 13.sp, fontWeight = FontWeight.Medium) 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) { Box(Modifier.size(8.dp).clip(CircleShape).background(dot)) Spacer(Modifier.width(6.dp)) @@ -338,14 +338,14 @@ private fun MemberDetailCard( Spacer(Modifier.height(6.dp)) Text("Connections:", fontSize = 13.sp, fontWeight = FontWeight.Medium) 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 arrowColor = if (lk.isServer) Color(0xFFFF9800) else Color(0xFF2196F3) + val arrowColor = if (lk.isServer) Color(0xFFFF9800) else Color(0xFF79B8FF) Column { Row(verticalAlignment = Alignment.CenterVertically) { Box(Modifier.size(8.dp).clip(CircleShape).background(dcol)) 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)) Text("${formatAddr(lk.localAddr)}:${lk.localPort}", modifier = Modifier.weight(1f), @@ -363,7 +363,7 @@ private fun MemberDetailCard( if (cleanSn.isNotEmpty()) { Row { 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) { Row(verticalAlignment = Alignment.CenterVertically) { 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)) Text("Direct: ${if (detail.directUp) "UP" else "DOWN"}" + (if (detail.directUp && detail.bestRtt > 0) " ${detail.bestRtt / 10}ms" else ""), @@ -386,7 +386,7 @@ private fun MemberDetailCard( if (detail.indirectPresence) { Row(verticalAlignment = Alignment.CenterVertically) { 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)) Text("Indirect: ${if (detail.indirectUp) "UP" else "DOWN"}", fontSize = 13.sp, color = Color.DarkGray) @@ -394,7 +394,7 @@ private fun MemberDetailCard( } if (detail.bgpPresence && detail.bgpNodes > 0) { 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)) Text("BGP: ${detail.bgpNodes} nodes" + (if (detail.bestRtt > 0) " ${detail.bestRtt / 10}ms" else ""), @@ -402,7 +402,7 @@ private fun MemberDetailCard( } } 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( "Admin/Super require channel private key", fontSize = 11.sp, - color = Color.Gray + color = Color(0xFFAEBCC8) ) } } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/PhotoRecordScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/PhotoRecordScreen.kt index 13997745..ded36e21 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/PhotoRecordScreen.kt +++ b/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 Surface( 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) } ) { Text( lens.label, - color = if (selected) Color.Black else Color.White, + color = Color(0xFFE6EDF3), fontSize = 13.sp, modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp) ) @@ -282,12 +282,12 @@ fun PhotoRecordScreen( val selected = r == selectedResolution Surface( 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 } ) { Text( r.label, - color = if (selected) Color.Black else Color.White, + color = Color(0xFFE6EDF3), fontSize = 13.sp, modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) ) @@ -308,7 +308,7 @@ fun PhotoRecordScreen( if (phase == PhotoPhase.CAPTURED) { Box( modifier = Modifier.size(52.dp) - .background(Color(0x66000000), CircleShape) + .background(Color(0xCC000000), CircleShape) .clickable { discardAndReset() }, contentAlignment = Alignment.Center ) { @@ -317,7 +317,7 @@ fun PhotoRecordScreen( Spacer(Modifier.width(28.dp)) Box( modifier = Modifier.size(52.dp) - .background(Color(0xFF4CAF50), CircleShape) + .background(Color(0xFF24633D), CircleShape) .clickable { outputFile?.let { f -> if (f.length() > 0) onSend(f.absolutePath, photoWidth, photoHeight) else onClose() } }, diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt index 9959b36f..a5ef86e5 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt +++ b/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) } else { 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)) { Row(verticalAlignment = Alignment.CenterVertically) { 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) Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { 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) Button(onClick = { runBlocking { provider.setServiceEnabled(true) } @@ -798,7 +798,7 @@ private fun AudioSettingsTab(vm: ChatViewModel) { LinearProgressIndicator( progress = { lvlPct }, 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, ) Button(onClick = { diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/VideoPlayerScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/VideoPlayerScreen.kt index 4fa1c06e..efa14c9e 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/VideoPlayerScreen.kt +++ b/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) { IconButton( 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( if (isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow, @@ -409,7 +409,7 @@ fun VideoPlayerScreen(filePath: String, onClose: () -> Unit) { onClick = { flipLandscape() }, modifier = Modifier.padding(start = 12.dp) .size(56.dp) - .background(Color(0x66000000), MaterialTheme.shapes.extraLarge) + .background(Color(0xCC000000), MaterialTheme.shapes.extraLarge) ) { Icon( Icons.Filled.ScreenRotation, @@ -427,7 +427,7 @@ fun VideoPlayerScreen(filePath: String, onClose: () -> Unit) { onClick = { flipLandscape() }, modifier = Modifier.padding(end = 12.dp) .size(56.dp) - .background(Color(0x66000000), MaterialTheme.shapes.extraLarge) + .background(Color(0xCC000000), MaterialTheme.shapes.extraLarge) ) { Icon( Icons.Filled.ScreenRotation, diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/VideoRecordScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/VideoRecordScreen.kt index 7b691fd3..ebb65769 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/VideoRecordScreen.kt +++ b/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 Surface( shape = RoundedCornerShape(16.dp), - color = if (selected) Color(0xFFFFD600) else Color(0x66000000), + color = if (selected) Color(0xFF214563) else Color(0xFF24323D), modifier = Modifier.clickable { selectedLens = lens bindCamera(lens.selector()) @@ -291,13 +291,13 @@ fun VideoRecordScreen( ) { Text( lens.label, - color = if (selected) Color.Black else Color.White, + color = Color(0xFFE6EDF3), fontSize = 13.sp ) if (lens.mm.isNotEmpty()) { Text( lens.mm, - color = (if (selected) Color.Black else Color.White).copy(alpha = 0.7f), + color = Color(0xFFAEBCC8), fontSize = 11.sp ) } @@ -311,12 +311,12 @@ fun VideoRecordScreen( val selected = q == quality Surface( 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 } ) { Text( q.label, - color = if (selected) Color.Black else Color.White, + color = Color(0xFFE6EDF3), fontSize = 13.sp, modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) ) @@ -346,7 +346,7 @@ fun VideoRecordScreen( /* discard / close */ Box( modifier = Modifier.size(52.dp) - .background(Color(0x66000000), CircleShape) + .background(Color(0xCC000000), CircleShape) .clickable { if (phase == Phase.IDLE) onClose() else discardAndReset() }, contentAlignment = Alignment.Center ) { @@ -358,7 +358,7 @@ fun VideoRecordScreen( Box( modifier = Modifier.size(76.dp) .background( - when (phase) { Phase.RECORDING -> Color(0xFFE53935); else -> Color(0xFFE53935) }, + when (phase) { Phase.RECORDING -> Color(0xFFB32632); else -> Color(0xFFB32632) }, CircleShape ) .clickable { @@ -389,7 +389,7 @@ fun VideoRecordScreen( val canSend = (phase == Phase.RECORDING || phase == Phase.PAUSED || phase == Phase.STOPPED) Box( 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) { if (phase == Phase.STOPPED) { outputFile?.let { onSend(it.absolutePath) } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/theme/Theme.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/theme/Theme.kt new file mode 100644 index 00000000..066e40c9 --- /dev/null +++ b/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) + } +} diff --git a/tools/chatgui-android/app/src/main/res/values/themes.xml b/tools/chatgui-android/app/src/main/res/values/themes.xml index fe7717fe..774a62d4 100644 --- a/tools/chatgui-android/app/src/main/res/values/themes.xml +++ b/tools/chatgui-android/app/src/main/res/values/themes.xml @@ -1,4 +1,12 @@ -