feat: Enhance logging and debugging capabilities across Protocol and UI components

This commit is contained in:
2026-01-09 00:34:45 +05:00
parent 28a0d7a601
commit 87cee5b9c3
7 changed files with 467 additions and 164 deletions

View File

@@ -24,7 +24,10 @@ enum class ProtocolState {
* Protocol client for Rosetta Messenger * Protocol client for Rosetta Messenger
* Handles WebSocket connection and packet exchange with server * Handles WebSocket connection and packet exchange with server
*/ */
class Protocol(private val serverAddress: String) { class Protocol(
private val serverAddress: String,
private val logger: (String) -> Unit = {}
) {
companion object { companion object {
private const val TAG = "RosettaProtocol" private const val TAG = "RosettaProtocol"
private const val RECONNECT_INTERVAL = 10000L // 10 seconds private const val RECONNECT_INTERVAL = 10000L // 10 seconds
@@ -32,6 +35,11 @@ class Protocol(private val serverAddress: String) {
private const val HANDSHAKE_TIMEOUT = 10000L // 10 seconds private const val HANDSHAKE_TIMEOUT = 10000L // 10 seconds
} }
private fun log(message: String) {
Log.d(TAG, message)
logger(message)
}
private val client = OkHttpClient.Builder() private val client = OkHttpClient.Builder()
.readTimeout(0, TimeUnit.MILLISECONDS) .readTimeout(0, TimeUnit.MILLISECONDS)
.connectTimeout(10, TimeUnit.SECONDS) .connectTimeout(10, TimeUnit.SECONDS)
@@ -75,7 +83,7 @@ class Protocol(private val serverAddress: String) {
// Register handshake response handler // Register handshake response handler
waitPacket(0x00) { packet -> waitPacket(0x00) { packet ->
if (packet is PacketHandshake) { if (packet is PacketHandshake) {
Log.d(TAG, "✅ Handshake response received, protocol version: ${packet.protocolVersion}") log("✅ Handshake response received, protocol version: ${packet.protocolVersion}")
handshakeJob?.cancel() handshakeJob?.cancel()
handshakeComplete = true handshakeComplete = true
_state.value = ProtocolState.AUTHENTICATED _state.value = ProtocolState.AUTHENTICATED
@@ -89,7 +97,7 @@ class Protocol(private val serverAddress: String) {
*/ */
fun connect() { fun connect() {
if (_state.value == ProtocolState.CONNECTING || _state.value == ProtocolState.CONNECTED) { if (_state.value == ProtocolState.CONNECTING || _state.value == ProtocolState.CONNECTED) {
Log.d(TAG, "Already connecting or connected") log("Already connecting or connected")
return return
} }
@@ -97,7 +105,7 @@ class Protocol(private val serverAddress: String) {
_state.value = ProtocolState.CONNECTING _state.value = ProtocolState.CONNECTING
_lastError.value = null _lastError.value = null
Log.d(TAG, "🔌 Connecting to: $serverAddress") log("🔌 Connecting to: $serverAddress")
val request = Request.Builder() val request = Request.Builder()
.url(serverAddress) .url(serverAddress)
@@ -105,7 +113,7 @@ class Protocol(private val serverAddress: String) {
webSocket = client.newWebSocket(request, object : WebSocketListener() { webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) { override fun onOpen(webSocket: WebSocket, response: Response) {
Log.d(TAG, "✅ WebSocket connected") log("✅ WebSocket connected")
reconnectAttempts = 0 reconnectAttempts = 0
_state.value = ProtocolState.CONNECTED _state.value = ProtocolState.CONNECTED
@@ -122,20 +130,20 @@ class Protocol(private val serverAddress: String) {
} }
override fun onMessage(webSocket: WebSocket, text: String) { override fun onMessage(webSocket: WebSocket, text: String) {
Log.d(TAG, "Received text message (unexpected): $text") log("Received text message (unexpected): $text")
} }
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
Log.d(TAG, "WebSocket closing: $code - $reason") log("WebSocket closing: $code - $reason")
} }
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.d(TAG, "WebSocket closed: $code - $reason") log("WebSocket closed: $code - $reason")
handleDisconnect() handleDisconnect()
} }
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.e(TAG, "❌ WebSocket error: ${t.message}") log("❌ WebSocket error: ${t.message}")
_lastError.value = t.message _lastError.value = t.message
handleDisconnect() handleDisconnect()
} }
@@ -146,16 +154,16 @@ class Protocol(private val serverAddress: String) {
* Start handshake with server * Start handshake with server
*/ */
fun startHandshake(publicKey: String, privateHash: String) { fun startHandshake(publicKey: String, privateHash: String) {
Log.d(TAG, "🤝 Starting handshake...") log("🤝 Starting handshake...")
Log.d(TAG, " Public key: ${publicKey.take(20)}...") log(" Public key: ${publicKey.take(20)}...")
Log.d(TAG, " Private hash: ${privateHash.take(20)}...") log(" Private hash: ${privateHash.take(20)}...")
// Save credentials for reconnection // Save credentials for reconnection
lastPublicKey = publicKey lastPublicKey = publicKey
lastPrivateHash = privateHash lastPrivateHash = privateHash
if (_state.value != ProtocolState.CONNECTED && _state.value != ProtocolState.AUTHENTICATED) { if (_state.value != ProtocolState.CONNECTED && _state.value != ProtocolState.AUTHENTICATED) {
Log.d(TAG, "Not connected, will handshake after connection") log("Not connected, will handshake after connection")
connect() connect()
return return
} }
@@ -175,7 +183,7 @@ class Protocol(private val serverAddress: String) {
handshakeJob = scope.launch { handshakeJob = scope.launch {
delay(HANDSHAKE_TIMEOUT) delay(HANDSHAKE_TIMEOUT)
if (!handshakeComplete) { if (!handshakeComplete) {
Log.e(TAG, "❌ Handshake timeout") log("❌ Handshake timeout")
_lastError.value = "Handshake timeout" _lastError.value = "Handshake timeout"
disconnect() disconnect()
} }
@@ -188,7 +196,7 @@ class Protocol(private val serverAddress: String) {
*/ */
fun sendPacket(packet: Packet) { fun sendPacket(packet: Packet) {
if (!handshakeComplete && packet !is PacketHandshake) { if (!handshakeComplete && packet !is PacketHandshake) {
Log.d(TAG, "📦 Queueing packet: ${packet.getPacketId()}") log("📦 Queueing packet: ${packet.getPacketId()}")
packetQueue.add(packet) packetQueue.add(packet)
return return
} }
@@ -199,13 +207,13 @@ class Protocol(private val serverAddress: String) {
val stream = packet.send() val stream = packet.send()
val data = stream.getStream() val data = stream.getStream()
Log.d(TAG, "📤 Sending packet: ${packet.getPacketId()} (${data.size} bytes)") log("📤 Sending packet: ${packet.getPacketId()} (${data.size} bytes)")
webSocket?.send(ByteString.of(*data)) webSocket?.send(ByteString.of(*data))
} }
private fun flushPacketQueue() { private fun flushPacketQueue() {
Log.d(TAG, "📬 Flushing ${packetQueue.size} queued packets") log("📬 Flushing ${packetQueue.size} queued packets")
val packets = packetQueue.toList() val packets = packetQueue.toList()
packetQueue.clear() packetQueue.clear()
packets.forEach { sendPacketDirect(it) } packets.forEach { sendPacketDirect(it) }
@@ -216,11 +224,11 @@ class Protocol(private val serverAddress: String) {
val stream = Stream(data) val stream = Stream(data)
val packetId = stream.readInt16() val packetId = stream.readInt16()
Log.d(TAG, "📥 Received packet: $packetId") log("📥 Received packet: $packetId")
val packetFactory = supportedPackets[packetId] val packetFactory = supportedPackets[packetId]
if (packetFactory == null) { if (packetFactory == null) {
Log.w(TAG, "Unknown packet ID: $packetId") log("⚠️ Unknown packet ID: $packetId")
return return
} }
@@ -232,11 +240,11 @@ class Protocol(private val serverAddress: String) {
try { try {
callback(packet) callback(packet)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error in packet handler: ${e.message}") log("Error in packet handler: ${e.message}")
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error parsing packet: ${e.message}") log("Error parsing packet: ${e.message}")
} }
} }
@@ -247,14 +255,14 @@ class Protocol(private val serverAddress: String) {
if (!isManuallyClosed && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { if (!isManuallyClosed && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++ reconnectAttempts++
Log.d(TAG, "🔄 Reconnecting in ${RECONNECT_INTERVAL}ms (attempt $reconnectAttempts/$MAX_RECONNECT_ATTEMPTS)") log("🔄 Reconnecting in ${RECONNECT_INTERVAL}ms (attempt $reconnectAttempts/$MAX_RECONNECT_ATTEMPTS)")
scope.launch { scope.launch {
delay(RECONNECT_INTERVAL) delay(RECONNECT_INTERVAL)
connect() connect()
} }
} else if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { } else if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
Log.e(TAG, "❌ Max reconnect attempts reached") log("❌ Max reconnect attempts reached")
_lastError.value = "Unable to connect to server" _lastError.value = "Unable to connect to server"
} }
} }
@@ -277,7 +285,7 @@ class Protocol(private val serverAddress: String) {
* Disconnect from server * Disconnect from server
*/ */
fun disconnect() { fun disconnect() {
Log.d(TAG, "Disconnecting...") log("Disconnecting...")
isManuallyClosed = true isManuallyClosed = true
handshakeJob?.cancel() handshakeJob?.cancel()
webSocket?.close(1000, "User disconnected") webSocket?.close(1000, "User disconnected")

View File

@@ -1,7 +1,11 @@
package com.rosetta.messenger.network package com.rosetta.messenger.network
import android.util.Log import android.util.Log
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.text.SimpleDateFormat
import java.util.*
/** /**
* Singleton manager for Protocol instance * Singleton manager for Protocol instance
@@ -15,13 +19,31 @@ object ProtocolManager {
private var protocol: Protocol? = null private var protocol: Protocol? = null
// Debug logs for dev console
private val _debugLogs = MutableStateFlow<List<String>>(emptyList())
val debugLogs: StateFlow<List<String>> = _debugLogs.asStateFlow()
private val dateFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
fun addLog(message: String) {
val timestamp = dateFormat.format(Date())
val logLine = "[$timestamp] $message"
Log.d(TAG, logLine)
_debugLogs.value = (_debugLogs.value + logLine).takeLast(100)
}
fun clearLogs() {
_debugLogs.value = emptyList()
}
/** /**
* Get or create Protocol instance * Get or create Protocol instance
*/ */
fun getProtocol(): Protocol { fun getProtocol(): Protocol {
if (protocol == null) { if (protocol == null) {
Log.d(TAG, "Creating new Protocol instance") addLog("Creating new Protocol instance")
protocol = Protocol(SERVER_ADDRESS) addLog("Server: $SERVER_ADDRESS")
protocol = Protocol(SERVER_ADDRESS) { msg -> addLog(msg) }
} }
return protocol!! return protocol!!
} }
@@ -42,6 +64,7 @@ object ProtocolManager {
* Connect to server * Connect to server
*/ */
fun connect() { fun connect() {
addLog("Connect requested")
getProtocol().connect() getProtocol().connect()
} }
@@ -49,7 +72,9 @@ object ProtocolManager {
* Authenticate with server * Authenticate with server
*/ */
fun authenticate(publicKey: String, privateHash: String) { fun authenticate(publicKey: String, privateHash: String) {
Log.d(TAG, "Authenticating...") addLog("Authenticate called")
addLog("PublicKey: ${publicKey.take(30)}...")
addLog("PrivateHash: ${privateHash.take(20)}...")
getProtocol().startHandshake(publicKey, privateHash) getProtocol().startHandshake(publicKey, privateHash)
} }

View File

@@ -63,8 +63,8 @@ fun SetPasswordScreen(
} }
val passwordsMatch = password == confirmPassword && password.isNotEmpty() val passwordsMatch = password == confirmPassword && password.isNotEmpty()
val passwordStrong = password.length >= 6 val isPasswordWeak = password.isNotEmpty() && password.length < 6
val canContinue = passwordsMatch && passwordStrong && !isCreating val canContinue = passwordsMatch && !isCreating
Box( Box(
modifier = Modifier modifier = Modifier
@@ -223,32 +223,60 @@ fun SetPasswordScreen(
animationSpec = tween(400, delayMillis = 350) animationSpec = tween(400, delayMillis = 350)
) )
) { ) {
Row( Column(modifier = Modifier.fillMaxWidth()) {
modifier = Modifier.fillMaxWidth(), Row(
verticalAlignment = Alignment.CenterVertically modifier = Modifier.fillMaxWidth(),
) { verticalAlignment = Alignment.CenterVertically
val strength = when { ) {
password.length < 6 -> "Weak" val strength = when {
password.length < 10 -> "Medium" password.length < 6 -> "Weak"
else -> "Strong" password.length < 10 -> "Medium"
else -> "Strong"
}
val strengthColor = when {
password.length < 6 -> Color(0xFFE53935)
password.length < 10 -> Color(0xFFFFA726)
else -> Color(0xFF4CAF50)
}
Icon(
imageVector = Icons.Default.Shield,
contentDescription = null,
tint = strengthColor,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "Password strength: $strength",
fontSize = 12.sp,
color = strengthColor
)
} }
val strengthColor = when { // Warning for weak passwords
password.length < 6 -> Color(0xFFE53935) if (isPasswordWeak) {
password.length < 10 -> Color(0xFFFFA726) Spacer(modifier = Modifier.height(4.dp))
else -> Color(0xFF4CAF50) Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFFE53935).copy(alpha = 0.1f))
.padding(8.dp),
verticalAlignment = Alignment.Top
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = null,
tint = Color(0xFFE53935),
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Your password is too weak. Consider using at least 6 characters for better security.",
fontSize = 11.sp,
color = Color(0xFFE53935),
lineHeight = 14.sp
)
}
} }
Icon(
imageVector = Icons.Default.Shield,
contentDescription = null,
tint = strengthColor,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "Password strength: $strength",
fontSize = 12.sp,
color = strengthColor
)
} }
} }
} }
@@ -394,10 +422,6 @@ fun SetPasswordScreen(
) { ) {
Button( Button(
onClick = { onClick = {
if (!passwordStrong) {
error = "Password must be at least 6 characters"
return@Button
}
if (!passwordsMatch) { if (!passwordsMatch) {
error = "Passwords don't match" error = "Passwords don't match"
return@Button return@Button

View File

@@ -117,8 +117,8 @@ fun WelcomeScreen(
) )
) { ) {
Text( Text(
text = "Rosetta uses cryptographic keys\nto secure your messages.\n\nNo account registration,\nno phone number required.", text = "Secure messaging with\ncryptographic keys",
fontSize = 15.sp, fontSize = 16.sp,
color = secondaryTextColor, color = secondaryTextColor,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
lineHeight = 24.sp, lineHeight = 24.sp,
@@ -126,14 +126,46 @@ fun WelcomeScreen(
) )
} }
Spacer(modifier = Modifier.weight(0.3f)) Spacer(modifier = Modifier.height(24.dp))
// Features list with icons - placed above buttons
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600, delayMillis = 400))
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
CompactFeatureItem(
icon = Icons.Default.Security,
text = "Encrypted",
isDarkTheme = isDarkTheme,
textColor = textColor
)
CompactFeatureItem(
icon = Icons.Default.NoAccounts,
text = "No Phone",
isDarkTheme = isDarkTheme,
textColor = textColor
)
CompactFeatureItem(
icon = Icons.Default.Key,
text = "Your Keys",
isDarkTheme = isDarkTheme,
textColor = textColor
)
}
}
Spacer(modifier = Modifier.height(32.dp))
// Create Seed Button // Create Seed Button
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = fadeIn(tween(600, delayMillis = 400)) + slideInVertically( enter = fadeIn(tween(600, delayMillis = 500)) + slideInVertically(
initialOffsetY = { 100 }, initialOffsetY = { 100 },
animationSpec = tween(600, delayMillis = 400) animationSpec = tween(600, delayMillis = 500)
) )
) { ) {
Button( Button(
@@ -145,14 +177,18 @@ fun WelcomeScreen(
containerColor = PrimaryBlue, containerColor = PrimaryBlue,
contentColor = Color.White contentColor = Color.White
), ),
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(16.dp),
elevation = ButtonDefaults.buttonElevation(
defaultElevation = 0.dp,
pressedElevation = 0.dp
)
) { ) {
Icon( Icon(
imageVector = Icons.Default.Key, imageVector = Icons.Default.Key,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(20.dp) modifier = Modifier.size(22.dp)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(12.dp))
Text( Text(
text = "Generate New Seed Phrase", text = "Generate New Seed Phrase",
fontSize = 16.sp, fontSize = 16.sp,
@@ -161,70 +197,35 @@ fun WelcomeScreen(
} }
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(12.dp))
// Import Seed Button // Import Seed Button
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = fadeIn(tween(600, delayMillis = 500)) + slideInVertically( enter = fadeIn(tween(600, delayMillis = 600)) + slideInVertically(
initialOffsetY = { 100 }, initialOffsetY = { 100 },
animationSpec = tween(600, delayMillis = 500) animationSpec = tween(600, delayMillis = 600)
) )
) { ) {
OutlinedButton( TextButton(
onClick = onImportSeed, onClick = onImportSeed,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(56.dp), .height(56.dp),
colors = ButtonDefaults.outlinedButtonColors( shape = RoundedCornerShape(16.dp)
contentColor = PrimaryBlue
),
border = ButtonDefaults.outlinedButtonBorder.copy(
brush = Brush.horizontalGradient(listOf(PrimaryBlue, PrimaryBlue))
),
shape = RoundedCornerShape(12.dp)
) { ) {
Icon( Icon(
imageVector = Icons.Default.Download, imageVector = Icons.Default.Download,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp),
tint = PrimaryBlue
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text( Text(
text = "I Already Have a Seed Phrase", text = "I Already Have a Seed Phrase",
fontSize = 16.sp, fontSize = 15.sp,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.Medium,
) color = PrimaryBlue
}
}
Spacer(modifier = Modifier.height(24.dp))
// Info text
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600, delayMillis = 600))
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(if (isDarkTheme) AuthSurface else AuthSurfaceLight)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "Your seed phrase is the master key to your account. Keep it safe and never share it.",
fontSize = 14.sp,
color = secondaryTextColor,
lineHeight = 18.sp
) )
} }
} }
@@ -233,3 +234,84 @@ fun WelcomeScreen(
} }
} }
} }
@Composable
private fun CompactFeatureItem(
icon: androidx.compose.ui.graphics.vector.ImageVector,
text: String,
isDarkTheme: Boolean,
textColor: Color
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(PrimaryBlue.copy(alpha = 0.12f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(24.dp)
)
}
Text(
text = text,
fontSize = 13.sp,
color = textColor.copy(alpha = 0.8f),
fontWeight = FontWeight.Medium,
textAlign = TextAlign.Center
)
}
}
@Composable
private fun FeatureItem(
icon: androidx.compose.ui.graphics.vector.ImageVector,
text: String,
isDarkTheme: Boolean,
textColor: Color
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(PrimaryBlue.copy(alpha = 0.15f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(20.dp)
)
}
Spacer(modifier = Modifier.width(16.dp))
Text(
text = text,
fontSize = 15.sp,
color = textColor,
fontWeight = FontWeight.Medium
)
}
}
)
}
Spacer(modifier = Modifier.width(16.dp))
Text(
text = text,
fontSize = 15.sp,
color = textColor,
fontWeight = FontWeight.Medium
)
}
}

View File

@@ -124,6 +124,12 @@ fun ChatsListScreen(
// Protocol connection state // Protocol connection state
val protocolState by ProtocolManager.state.collectAsState() val protocolState by ProtocolManager.state.collectAsState()
val debugLogs by ProtocolManager.debugLogs.collectAsState()
// Dev console state
var showDevConsole by remember { mutableStateOf(false) }
var titleClickCount by remember { mutableStateOf(0) }
var lastClickTime by remember { mutableStateOf(0L) }
var visible by remember { mutableStateOf(false) } var visible by remember { mutableStateOf(false) }
@@ -131,6 +137,95 @@ fun ChatsListScreen(
visible = true visible = true
} }
// Dev console dialog
if (showDevConsole) {
AlertDialog(
onDismissRequest = { showDevConsole = false },
title = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Dev Console", fontWeight = FontWeight.Bold)
Text(
text = protocolState.name,
fontSize = 12.sp,
color = when (protocolState) {
ProtocolState.AUTHENTICATED -> Color(0xFF4CAF50)
ProtocolState.CONNECTING, ProtocolState.HANDSHAKING -> Color(0xFFFFA726)
else -> Color(0xFFFF5722)
}
)
}
},
text = {
Column {
Box(
modifier = Modifier
.fillMaxWidth()
.height(400.dp)
.background(Color(0xFF1A1A1A), RoundedCornerShape(8.dp))
.padding(8.dp)
) {
val scrollState = rememberScrollState()
LaunchedEffect(debugLogs.size) {
scrollState.animateScrollTo(scrollState.maxValue)
}
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
) {
if (debugLogs.isEmpty()) {
Text(
"No logs yet...",
color = Color.Gray,
fontSize = 12.sp
)
} else {
debugLogs.forEach { log ->
Text(
text = log,
color = when {
log.contains("") -> Color(0xFF4CAF50)
log.contains("") -> Color(0xFFFF5722)
log.contains("⚠️") -> Color(0xFFFFA726)
log.contains("📤") -> Color(0xFF2196F3)
log.contains("📥") -> Color(0xFF9C27B0)
else -> Color.White
},
fontSize = 11.sp,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
modifier = Modifier.padding(vertical = 1.dp)
)
}
}
}
}
}
},
confirmButton = {
Row {
TextButton(onClick = { ProtocolManager.clearLogs() }) {
Text("Clear")
}
TextButton(onClick = {
ProtocolManager.connect()
}) {
Text("Reconnect")
}
TextButton(onClick = { showDevConsole = false }) {
Text("Close")
}
}
},
containerColor = if (isDarkTheme) Color(0xFF212121) else Color.White
)
}
// Drawer menu items // Drawer menu items
val menuItems = listOf( val menuItems = listOf(
DrawerMenuItem( DrawerMenuItem(
@@ -211,17 +306,73 @@ fun ChatsListScreen(
} }
// Menu items // Menu items
menuItems.forEach { item -> Column(
DrawerItem( modifier = Modifier
icon = item.icon, .fillMaxHeight()
title = item.title, .weight(1f)
onClick = { ) {
Spacer(modifier = Modifier.height(8.dp))
menuItems.forEachIndexed { index, item ->
DrawerItem(
icon = item.icon,
title = item.title,
onClick = {
scope.launch { drawerState.close() }
item.onClick()
},
isDarkTheme = isDarkTheme
)
// Add separator between items (except after last)
if (index < menuItems.size - 1) {
Divider(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
thickness = 0.5.dp,
color = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8)
)
}
}
}
// Logout button at bottom
Divider(
modifier = Modifier.padding(horizontal = 16.dp),
thickness = 0.5.dp,
color = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8)
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp)
.clip(RoundedCornerShape(12.dp))
.background(Color(0x20FF3B30))
.clickable {
scope.launch { drawerState.close() } scope.launch { drawerState.close() }
item.onClick() onLogout()
}, }
isDarkTheme = isDarkTheme .padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Default.Logout,
contentDescription = "Logout",
tint = Color(0xFFFF3B30),
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = "Log Out",
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFFFF3B30)
) )
} }
Spacer(modifier = Modifier.height(16.dp))
} }
} }
) { ) {
@@ -247,9 +398,22 @@ fun ChatsListScreen(
} }
}, },
title = { title = {
// Stories / Title area // Stories / Title area - Triple click to open dev console
Row( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable {
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime < 500) {
titleClickCount++
if (titleClickCount >= 3) {
showDevConsole = true
titleClickCount = 0
}
} else {
titleClickCount = 1
}
lastClickTime = currentTime
}
) { ) {
// User story avatar placeholder // User story avatar placeholder
Box( Box(

View File

@@ -98,13 +98,11 @@ fun OnboardingScreen(
val elapsed = System.currentTimeMillis() - startTime val elapsed = System.currentTimeMillis() - startTime
transitionProgress = (elapsed / duration).coerceAtMost(1f) transitionProgress = (elapsed / duration).coerceAtMost(1f)
// Update status bar when wave reaches top (around 15% progress)
if (transitionProgress >= 0.15f && !shouldUpdateStatusBar) {
shouldUpdateStatusBar = true
}
delay(16) // ~60fps delay(16) // ~60fps
} }
// Update status bar icons after animation is completely finished
shouldUpdateStatusBar = true
delay(50) // Small delay to ensure UI updates
isTransitioning = false isTransitioning = false
transitionProgress = 0f transitionProgress = 0f
shouldUpdateStatusBar = false shouldUpdateStatusBar = false
@@ -112,9 +110,34 @@ fun OnboardingScreen(
} }
} }
// Update status bar and navigation bar icons when wave reaches the top // Animate navigation bar color starting at 80% of wave animation
val view = LocalView.current val view = LocalView.current
LaunchedEffect(shouldUpdateStatusBar, isDarkTheme) { LaunchedEffect(isTransitioning, transitionProgress) {
if (isTransitioning && transitionProgress >= 0.8f && !view.isInEditMode) {
val window = (view.context as android.app.Activity).window
// Map 0.8-1.0 to 0-1 for smooth interpolation
val navProgress = ((transitionProgress - 0.8f) / 0.2f).coerceIn(0f, 1f)
val oldColor = if (previousTheme) 0xFF1E1E1E else 0xFFFFFFFF
val newColor = if (targetTheme) 0xFF1E1E1E else 0xFFFFFFFF
val r1 = (oldColor shr 16 and 0xFF)
val g1 = (oldColor shr 8 and 0xFF)
val b1 = (oldColor and 0xFF)
val r2 = (newColor shr 16 and 0xFF)
val g2 = (newColor shr 8 and 0xFF)
val b2 = (newColor and 0xFF)
val r = (r1 + (r2 - r1) * navProgress).toInt()
val g = (g1 + (g2 - g1) * navProgress).toInt()
val b = (b1 + (b2 - b1) * navProgress).toInt()
window.navigationBarColor = (0xFF000000 or (r.toLong() shl 16) or (g.toLong() shl 8) or b.toLong()).toInt()
}
}
// Update status bar icons when animation finishes
LaunchedEffect(shouldUpdateStatusBar) {
if (shouldUpdateStatusBar && !view.isInEditMode) { if (shouldUpdateStatusBar && !view.isInEditMode) {
val window = (view.context as android.app.Activity).window val window = (view.context as android.app.Activity).window
val insetsController = WindowCompat.getInsetsController(window, view) val insetsController = WindowCompat.getInsetsController(window, view)
@@ -124,32 +147,11 @@ fun OnboardingScreen(
} }
} }
// Animate navigation bar color with theme transition // Set initial navigation bar color only on first launch
LaunchedEffect(isTransitioning, transitionProgress, isDarkTheme) { LaunchedEffect(Unit) {
if (!view.isInEditMode) { if (!view.isInEditMode) {
val window = (view.context as android.app.Activity).window val window = (view.context as android.app.Activity).window
if (isTransitioning) { window.navigationBarColor = if (isDarkTheme) 0xFF1E1E1E.toInt() else 0xFFFFFFFF.toInt()
// Interpolate color during transition
val oldColor = if (previousTheme) 0xFF1E1E1E else 0xFFFFFFFF
val newColor = if (targetTheme) 0xFF1E1E1E else 0xFFFFFFFF
val r1 = (oldColor shr 16 and 0xFF)
val g1 = (oldColor shr 8 and 0xFF)
val b1 = (oldColor and 0xFF)
val r2 = (newColor shr 16 and 0xFF)
val g2 = (newColor shr 8 and 0xFF)
val b2 = (newColor and 0xFF)
val r = (r1 + (r2 - r1) * transitionProgress).toInt()
val g = (g1 + (g2 - g1) * transitionProgress).toInt()
val b = (b1 + (b2 - b1) * transitionProgress).toInt()
window.navigationBarColor = (0xFF000000 or (r.toLong() shl 16) or (g.toLong() shl 8) or b.toLong()).toInt()
} else {
// Set final color when not transitioning
window.navigationBarColor = if (isDarkTheme) 0xFF1E1E1E.toInt() else 0xFFFFFFFF.toInt()
}
} }
} }
@@ -387,42 +389,40 @@ fun AnimatedRosettaLogo(
) )
Box( Box(
modifier = modifier modifier = modifier,
.scale(scale)
.graphicsLayer { this.alpha = alpha },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
// Pre-render all animations to avoid lag // Pre-render all animations to avoid lag
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
// Rosetta icon (page 0) with pulse animation // Rosetta icon (page 0) with pulse animation like splash screen
if (currentPage == 0) { if (currentPage == 0) {
val pulseScale by rememberInfiniteTransition(label = "pulse").animateFloat( val pulseScale by rememberInfiniteTransition(label = "pulse").animateFloat(
initialValue = 1f, initialValue = 1f,
targetValue = 1.08f, targetValue = 1.1f,
animationSpec = infiniteRepeatable( animationSpec = infiniteRepeatable(
animation = tween(1000, easing = FastOutSlowInEasing), animation = tween(800, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse repeatMode = RepeatMode.Reverse
), ),
label = "pulseScale" label = "pulseScale"
) )
// Glow effect behind logo - separate Box without clipping // Glow effect behind logo - same style as splash screen
Box( Box(
modifier = Modifier modifier = Modifier
.size(200.dp) .size(180.dp)
.scale(pulseScale) .scale(pulseScale)
.background( .background(
color = Color(0xFF54A9EB).copy(alpha = 0.15f), color = Color(0xFF54A9EB).copy(alpha = 0.2f),
shape = CircleShape shape = CircleShape
) )
) )
// Main logo - circular like splash screen
Image( Image(
painter = painterResource(id = R.drawable.rosetta_icon), painter = painterResource(id = R.drawable.rosetta_icon),
contentDescription = "Rosetta Logo", contentDescription = "Rosetta Logo",
modifier = Modifier modifier = Modifier
.size(180.dp) .size(150.dp)
.scale(pulseScale)
.clip(CircleShape) .clip(CircleShape)
) )
} }

View File

@@ -68,9 +68,9 @@ fun RosettaAndroidTheme(
val window = (view.context as android.app.Activity).window val window = (view.context as android.app.Activity).window
// Make status bar transparent for wave animation overlay // Make status bar transparent for wave animation overlay
window.statusBarColor = AndroidColor.TRANSPARENT window.statusBarColor = AndroidColor.TRANSPARENT
window.navigationBarColor = if (darkTheme) 0xFF1B1B1B.toInt() else 0xFFFFFFFF.toInt() // Navigation bar color is managed by OnboardingScreen for smooth transition
// Don't change it here to avoid instant color change
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
WindowCompat.getInsetsController(window, view).isAppearanceLightNavigationBars = !darkTheme
} }
} }