feat: Enhance logging and debugging capabilities across Protocol and UI components
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user