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

This commit is contained in:
k1ngsterr1
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
* 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 {
private const val TAG = "RosettaProtocol"
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 fun log(message: String) {
Log.d(TAG, message)
logger(message)
}
private val client = OkHttpClient.Builder()
.readTimeout(0, TimeUnit.MILLISECONDS)
.connectTimeout(10, TimeUnit.SECONDS)
@@ -75,7 +83,7 @@ class Protocol(private val serverAddress: String) {
// Register handshake response handler
waitPacket(0x00) { packet ->
if (packet is PacketHandshake) {
Log.d(TAG, "✅ Handshake response received, protocol version: ${packet.protocolVersion}")
log("✅ Handshake response received, protocol version: ${packet.protocolVersion}")
handshakeJob?.cancel()
handshakeComplete = true
_state.value = ProtocolState.AUTHENTICATED
@@ -89,7 +97,7 @@ class Protocol(private val serverAddress: String) {
*/
fun connect() {
if (_state.value == ProtocolState.CONNECTING || _state.value == ProtocolState.CONNECTED) {
Log.d(TAG, "Already connecting or connected")
log("Already connecting or connected")
return
}
@@ -97,7 +105,7 @@ class Protocol(private val serverAddress: String) {
_state.value = ProtocolState.CONNECTING
_lastError.value = null
Log.d(TAG, "🔌 Connecting to: $serverAddress")
log("🔌 Connecting to: $serverAddress")
val request = Request.Builder()
.url(serverAddress)
@@ -105,7 +113,7 @@ class Protocol(private val serverAddress: String) {
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
Log.d(TAG, "✅ WebSocket connected")
log("✅ WebSocket connected")
reconnectAttempts = 0
_state.value = ProtocolState.CONNECTED
@@ -122,20 +130,20 @@ class Protocol(private val serverAddress: 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) {
Log.d(TAG, "WebSocket closing: $code - $reason")
log("WebSocket closing: $code - $reason")
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.d(TAG, "WebSocket closed: $code - $reason")
log("WebSocket closed: $code - $reason")
handleDisconnect()
}
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
handleDisconnect()
}
@@ -146,16 +154,16 @@ class Protocol(private val serverAddress: String) {
* Start handshake with server
*/
fun startHandshake(publicKey: String, privateHash: String) {
Log.d(TAG, "🤝 Starting handshake...")
Log.d(TAG, " Public key: ${publicKey.take(20)}...")
Log.d(TAG, " Private hash: ${privateHash.take(20)}...")
log("🤝 Starting handshake...")
log(" Public key: ${publicKey.take(20)}...")
log(" Private hash: ${privateHash.take(20)}...")
// Save credentials for reconnection
lastPublicKey = publicKey
lastPrivateHash = privateHash
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()
return
}
@@ -175,7 +183,7 @@ class Protocol(private val serverAddress: String) {
handshakeJob = scope.launch {
delay(HANDSHAKE_TIMEOUT)
if (!handshakeComplete) {
Log.e(TAG, "❌ Handshake timeout")
log("❌ Handshake timeout")
_lastError.value = "Handshake timeout"
disconnect()
}
@@ -188,7 +196,7 @@ class Protocol(private val serverAddress: String) {
*/
fun sendPacket(packet: Packet) {
if (!handshakeComplete && packet !is PacketHandshake) {
Log.d(TAG, "📦 Queueing packet: ${packet.getPacketId()}")
log("📦 Queueing packet: ${packet.getPacketId()}")
packetQueue.add(packet)
return
}
@@ -199,13 +207,13 @@ class Protocol(private val serverAddress: String) {
val stream = packet.send()
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))
}
private fun flushPacketQueue() {
Log.d(TAG, "📬 Flushing ${packetQueue.size} queued packets")
log("📬 Flushing ${packetQueue.size} queued packets")
val packets = packetQueue.toList()
packetQueue.clear()
packets.forEach { sendPacketDirect(it) }
@@ -216,11 +224,11 @@ class Protocol(private val serverAddress: String) {
val stream = Stream(data)
val packetId = stream.readInt16()
Log.d(TAG, "📥 Received packet: $packetId")
log("📥 Received packet: $packetId")
val packetFactory = supportedPackets[packetId]
if (packetFactory == null) {
Log.w(TAG, "Unknown packet ID: $packetId")
log("⚠️ Unknown packet ID: $packetId")
return
}
@@ -232,11 +240,11 @@ class Protocol(private val serverAddress: String) {
try {
callback(packet)
} catch (e: Exception) {
Log.e(TAG, "Error in packet handler: ${e.message}")
log("Error in packet handler: ${e.message}")
}
}
} 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) {
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 {
delay(RECONNECT_INTERVAL)
connect()
}
} 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"
}
}
@@ -277,7 +285,7 @@ class Protocol(private val serverAddress: String) {
* Disconnect from server
*/
fun disconnect() {
Log.d(TAG, "Disconnecting...")
log("Disconnecting...")
isManuallyClosed = true
handshakeJob?.cancel()
webSocket?.close(1000, "User disconnected")

View File

@@ -1,7 +1,11 @@
package com.rosetta.messenger.network
import android.util.Log
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.text.SimpleDateFormat
import java.util.*
/**
* Singleton manager for Protocol instance
@@ -15,13 +19,31 @@ object ProtocolManager {
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
*/
fun getProtocol(): Protocol {
if (protocol == null) {
Log.d(TAG, "Creating new Protocol instance")
protocol = Protocol(SERVER_ADDRESS)
addLog("Creating new Protocol instance")
addLog("Server: $SERVER_ADDRESS")
protocol = Protocol(SERVER_ADDRESS) { msg -> addLog(msg) }
}
return protocol!!
}
@@ -42,6 +64,7 @@ object ProtocolManager {
* Connect to server
*/
fun connect() {
addLog("Connect requested")
getProtocol().connect()
}
@@ -49,7 +72,9 @@ object ProtocolManager {
* Authenticate with server
*/
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)
}

View File

@@ -63,8 +63,8 @@ fun SetPasswordScreen(
}
val passwordsMatch = password == confirmPassword && password.isNotEmpty()
val passwordStrong = password.length >= 6
val canContinue = passwordsMatch && passwordStrong && !isCreating
val isPasswordWeak = password.isNotEmpty() && password.length < 6
val canContinue = passwordsMatch && !isCreating
Box(
modifier = Modifier
@@ -223,32 +223,60 @@ fun SetPasswordScreen(
animationSpec = tween(400, delayMillis = 350)
)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
val strength = when {
password.length < 6 -> "Weak"
password.length < 10 -> "Medium"
else -> "Strong"
Column(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
val strength = when {
password.length < 6 -> "Weak"
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 {
password.length < 6 -> Color(0xFFE53935)
password.length < 10 -> Color(0xFFFFA726)
else -> Color(0xFF4CAF50)
// Warning for weak passwords
if (isPasswordWeak) {
Spacer(modifier = Modifier.height(4.dp))
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(
onClick = {
if (!passwordStrong) {
error = "Password must be at least 6 characters"
return@Button
}
if (!passwordsMatch) {
error = "Passwords don't match"
return@Button

View File

@@ -117,8 +117,8 @@ fun WelcomeScreen(
)
) {
Text(
text = "Rosetta uses cryptographic keys\nto secure your messages.\n\nNo account registration,\nno phone number required.",
fontSize = 15.sp,
text = "Secure messaging with\ncryptographic keys",
fontSize = 16.sp,
color = secondaryTextColor,
textAlign = TextAlign.Center,
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
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600, delayMillis = 400)) + slideInVertically(
enter = fadeIn(tween(600, delayMillis = 500)) + slideInVertically(
initialOffsetY = { 100 },
animationSpec = tween(600, delayMillis = 400)
animationSpec = tween(600, delayMillis = 500)
)
) {
Button(
@@ -145,14 +177,18 @@ fun WelcomeScreen(
containerColor = PrimaryBlue,
contentColor = Color.White
),
shape = RoundedCornerShape(12.dp)
shape = RoundedCornerShape(16.dp),
elevation = ButtonDefaults.buttonElevation(
defaultElevation = 0.dp,
pressedElevation = 0.dp
)
) {
Icon(
imageVector = Icons.Default.Key,
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 = "Generate New Seed Phrase",
fontSize = 16.sp,
@@ -161,70 +197,35 @@ fun WelcomeScreen(
}
}
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(12.dp))
// Import Seed Button
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600, delayMillis = 500)) + slideInVertically(
enter = fadeIn(tween(600, delayMillis = 600)) + slideInVertically(
initialOffsetY = { 100 },
animationSpec = tween(600, delayMillis = 500)
animationSpec = tween(600, delayMillis = 600)
)
) {
OutlinedButton(
TextButton(
onClick = onImportSeed,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = PrimaryBlue
),
border = ButtonDefaults.outlinedButtonBorder.copy(
brush = Brush.horizontalGradient(listOf(PrimaryBlue, PrimaryBlue))
),
shape = RoundedCornerShape(12.dp)
shape = RoundedCornerShape(16.dp)
) {
Icon(
imageVector = Icons.Default.Download,
contentDescription = null,
modifier = Modifier.size(20.dp)
modifier = Modifier.size(20.dp),
tint = PrimaryBlue
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "I Already Have a Seed Phrase",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
}
}
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
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
color = PrimaryBlue
)
}
}
@@ -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
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) }
@@ -131,6 +137,95 @@ fun ChatsListScreen(
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
val menuItems = listOf(
DrawerMenuItem(
@@ -211,17 +306,73 @@ fun ChatsListScreen(
}
// Menu items
menuItems.forEach { item ->
DrawerItem(
icon = item.icon,
title = item.title,
onClick = {
Column(
modifier = Modifier
.fillMaxHeight()
.weight(1f)
) {
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() }
item.onClick()
},
isDarkTheme = isDarkTheme
onLogout()
}
.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 = {
// Stories / Title area
// Stories / Title area - Triple click to open dev console
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
Box(

View File

@@ -98,13 +98,11 @@ fun OnboardingScreen(
val elapsed = System.currentTimeMillis() - startTime
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
}
// Update status bar icons after animation is completely finished
shouldUpdateStatusBar = true
delay(50) // Small delay to ensure UI updates
isTransitioning = false
transitionProgress = 0f
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
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) {
val window = (view.context as android.app.Activity).window
val insetsController = WindowCompat.getInsetsController(window, view)
@@ -124,32 +147,11 @@ fun OnboardingScreen(
}
}
// Animate navigation bar color with theme transition
LaunchedEffect(isTransitioning, transitionProgress, isDarkTheme) {
// Set initial navigation bar color only on first launch
LaunchedEffect(Unit) {
if (!view.isInEditMode) {
val window = (view.context as android.app.Activity).window
if (isTransitioning) {
// 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()
}
window.navigationBarColor = if (isDarkTheme) 0xFF1E1E1E.toInt() else 0xFFFFFFFF.toInt()
}
}
@@ -387,42 +389,40 @@ fun AnimatedRosettaLogo(
)
Box(
modifier = modifier
.scale(scale)
.graphicsLayer { this.alpha = alpha },
modifier = modifier,
contentAlignment = Alignment.Center
) {
// Pre-render all animations to avoid lag
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) {
val pulseScale by rememberInfiniteTransition(label = "pulse").animateFloat(
initialValue = 1f,
targetValue = 1.08f,
targetValue = 1.1f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = FastOutSlowInEasing),
animation = tween(800, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "pulseScale"
)
// Glow effect behind logo - separate Box without clipping
// Glow effect behind logo - same style as splash screen
Box(
modifier = Modifier
.size(200.dp)
.size(180.dp)
.scale(pulseScale)
.background(
color = Color(0xFF54A9EB).copy(alpha = 0.15f),
color = Color(0xFF54A9EB).copy(alpha = 0.2f),
shape = CircleShape
)
)
// Main logo - circular like splash screen
Image(
painter = painterResource(id = R.drawable.rosetta_icon),
contentDescription = "Rosetta Logo",
modifier = Modifier
.size(180.dp)
.scale(pulseScale)
.size(150.dp)
.clip(CircleShape)
)
}

View File

@@ -68,9 +68,9 @@ fun RosettaAndroidTheme(
val window = (view.context as android.app.Activity).window
// Make status bar transparent for wave animation overlay
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).isAppearanceLightNavigationBars = !darkTheme
}
}