feat: Enhance architecture and performance optimizations

- Updated architecture documentation to reflect changes in data layer and caching strategies.
- Implemented LRU caching for key pair generation and private key hash to improve performance.
- Refactored DatabaseService to include LRU caching for encrypted accounts, reducing database query times.
- Introduced a search bar in SelectAccountScreen for filtering accounts, enhancing user experience.
- Adjusted UI components for better spacing and consistency.
- Updated build.gradle.kts to support Java 17 and Room incremental annotation processing.
- Modified gradle.properties to include necessary JVM arguments for Java 17 compatibility.
This commit is contained in:
k1ngsterr1
2026-01-09 01:33:30 +05:00
parent 2f77c16484
commit 3ae544dac2
6 changed files with 768 additions and 179 deletions

View File

@@ -172,7 +172,10 @@ class DatabaseService(context: Context) {
CryptoManager.decryptWithPassword(
encryptedAccount.privateKeyEncrypted,
password
)
) ?: run {
Log.e(TAG, "❌ Failed to decrypt private key - returned null")
return@withContext null
}
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to decrypt private key - wrong password?", e)
return@withContext null
@@ -183,7 +186,10 @@ class DatabaseService(context: Context) {
CryptoManager.decryptWithPassword(
encryptedAccount.seedPhraseEncrypted,
password
)
) ?: run {
Log.e(TAG, "❌ Failed to decrypt seed phrase - returned null")
return@withContext null
}
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to decrypt seed phrase - wrong password?", e)
return@withContext null

View File

@@ -292,9 +292,6 @@ class AuthStateManager(
*/
fun getCurrentAccount(): DecryptedAccountData? = currentDecryptedAccount
}
}
}
}
@Composable
fun rememberAuthState(context: Context): AuthStateManager {

View File

@@ -4,9 +4,8 @@ import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
@@ -31,16 +30,20 @@ data class AccountInfo(
val publicKey: String
)
// Avatar colors for accounts
// Avatar colors matching React Native app (Mantine inspired)
// Using primary colors (same as text colors from light theme for consistency)
private val accountColors = listOf(
Color(0xFF5E9FFF), // Blue
Color(0xFFFF7EB3), // Pink
Color(0xFF7B68EE), // Purple
Color(0xFF50C878), // Green
Color(0xFFFF6B6B), // Red
Color(0xFF4ECDC4), // Teal
Color(0xFFFFB347), // Orange
Color(0xFFBA55D3) // Orchid
Color(0xFF1971c2), // blue
Color(0xFF0c8599), // cyan
Color(0xFF9c36b5), // grape
Color(0xFF2f9e44), // green
Color(0xFF4263eb), // indigo
Color(0xFF5c940d), // lime
Color(0xFFd9480f), // orange
Color(0xFFc2255c), // pink
Color(0xFFe03131), // red
Color(0xFF099268), // teal
Color(0xFF6741d9) // violet
)
fun getAccountColor(name: String): Color {
@@ -63,9 +66,23 @@ fun SelectAccountScreen(
val backgroundColor = if (isDarkTheme) Color(0xFF1E1E1E) else Color(0xFFFFFFFF)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val searchBarColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
var searchQuery by remember { mutableStateOf("") }
var visible by remember { mutableStateOf(false) }
// Фильтрация аккаунтов по поиску
val filteredAccounts = remember(accounts, searchQuery) {
if (searchQuery.isEmpty()) {
accounts
} else {
accounts.filter { account ->
account.name.contains(searchQuery, ignoreCase = true) ||
account.publicKey.contains(searchQuery, ignoreCase = true)
}
}
}
LaunchedEffect(Unit) {
visible = true
}
@@ -79,9 +96,9 @@ fun SelectAccountScreen(
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 32.dp)
.padding(horizontal = 24.dp)
) {
Spacer(modifier = Modifier.height(60.dp))
Spacer(modifier = Modifier.height(40.dp))
// Header
AnimatedVisibility(
@@ -110,7 +127,7 @@ fun SelectAccountScreen(
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Select your account for login,\nor add new account",
text = "Choose account to login",
fontSize = 15.sp,
color = secondaryTextColor,
lineHeight = 22.sp
@@ -118,37 +135,100 @@ fun SelectAccountScreen(
}
}
Spacer(modifier = Modifier.height(40.dp))
Spacer(modifier = Modifier.height(24.dp))
// Accounts grid
// Search bar
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 150)) + expandVertically(
animationSpec = tween(500, delayMillis = 150)
)
) {
OutlinedTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
placeholder = {
Text(
text = "Search accounts...",
color = secondaryTextColor
)
},
leadingIcon = {
Icon(
Icons.Default.Search,
contentDescription = "Search",
tint = secondaryTextColor
)
},
trailingIcon = {
if (searchQuery.isNotEmpty()) {
IconButton(onClick = { searchQuery = "" }) {
Icon(
Icons.Default.Clear,
contentDescription = "Clear",
tint = secondaryTextColor
)
}
}
},
colors = OutlinedTextFieldDefaults.colors(
unfocusedContainerColor = searchBarColor,
focusedContainerColor = searchBarColor,
unfocusedBorderColor = Color.Transparent,
focusedBorderColor = PrimaryBlue,
cursorColor = PrimaryBlue,
focusedTextColor = textColor,
unfocusedTextColor = textColor
),
shape = RoundedCornerShape(12.dp),
singleLine = true
)
}
Spacer(modifier = Modifier.height(16.dp))
// Accounts list
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 200))
) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
horizontalArrangement = Arrangement.spacedBy(12.dp),
LazyColumn(
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.fillMaxWidth()
) {
items(accounts, key = { it.id }) { account ->
val index = accounts.indexOf(account)
AccountCard(
items(filteredAccounts, key = { it.id }) { account ->
val index = filteredAccounts.indexOf(account)
AccountListItem(
account = account,
isSelected = account.id == selectedAccountId,
isDarkTheme = isDarkTheme,
onClick = { onSelectAccount(account.id) },
animationDelay = 250 + (index * 100)
animationDelay = 250 + (index * 50)
)
}
// Add Account card
// Empty state
if (filteredAccounts.isEmpty() && searchQuery.isNotEmpty()) {
item {
EmptySearchResult(
isDarkTheme = isDarkTheme,
searchQuery = searchQuery
)
}
}
// Add Account button
item {
AddAccountCard(
Spacer(modifier = Modifier.height(8.dp))
AddAccountButton(
isDarkTheme = isDarkTheme,
onClick = onAddAccount,
animationDelay = 250 + (accounts.size * 100)
animationDelay = 250 + (filteredAccounts.size * 50)
)
Spacer(modifier = Modifier.height(24.dp))
}
}
}
@@ -167,7 +247,7 @@ fun SelectAccountScreen(
}
@Composable
private fun AccountCard(
private fun AccountListItem(
account: AccountInfo,
isSelected: Boolean,
isDarkTheme: Boolean,
@@ -175,7 +255,6 @@ private fun AccountCard(
animationDelay: Int
) {
val surfaceColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
val borderColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
@@ -190,89 +269,90 @@ private fun AccountCard(
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(400)) + scaleIn(
initialScale = 0.9f,
enter = fadeIn(tween(400)) + slideInHorizontally(
initialOffsetX = { -50 },
animationSpec = tween(400)
)
) {
Card(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(0.85f)
.height(80.dp)
.clickable(onClick = onClick),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = if (isSelected) PrimaryBlue.copy(alpha = 0.1f) else surfaceColor
),
border = BorderStroke(
width = 2.dp,
color = if (isSelected) PrimaryBlue else borderColor
)
border = if (isSelected) BorderStroke(2.dp, PrimaryBlue) else null
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
Row(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Checkmark
// Avatar
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(avatarColor.copy(alpha = 0.2f)),
contentAlignment = Alignment.Center
) {
Text(
text = account.initials,
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
color = avatarColor
)
}
Spacer(modifier = Modifier.width(16.dp))
// Account info
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = account.name,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = if (isSelected) PrimaryBlue else textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "${account.publicKey.take(8)}...${account.publicKey.takeLast(6)}",
fontSize = 13.sp,
color = secondaryTextColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
// Selected indicator
if (isSelected) {
Box(
modifier = Modifier
.align(Alignment.TopStart)
.padding(12.dp)
.size(24.dp)
.size(28.dp)
.clip(CircleShape)
.background(PrimaryBlue),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Check,
contentDescription = null,
contentDescription = "Selected",
tint = Color.White,
modifier = Modifier.size(14.dp)
modifier = Modifier.size(16.dp)
)
}
} else {
Box(
modifier = Modifier
.align(Alignment.TopStart)
.padding(12.dp)
.size(24.dp)
.clip(CircleShape)
.border(2.dp, borderColor, CircleShape)
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(20.dp)
) {
// Avatar
Box(
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.background(avatarColor.copy(alpha = 0.2f)),
contentAlignment = Alignment.Center
) {
Text(
text = account.initials,
fontSize = 28.sp,
fontWeight = FontWeight.SemiBold,
color = avatarColor
)
}
Spacer(modifier = Modifier.height(16.dp))
// Name
Text(
text = account.name,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = if (isSelected) PrimaryBlue else textColor,
textAlign = TextAlign.Center,
maxLines = 2,
overflow = TextOverflow.Ellipsis
Icon(
Icons.Default.ArrowForward,
contentDescription = "Select",
tint = secondaryTextColor,
modifier = Modifier.size(24.dp)
)
}
}
@@ -281,14 +361,12 @@ private fun AccountCard(
}
@Composable
private fun AddAccountCard(
private fun AddAccountButton(
isDarkTheme: Boolean,
onClick: () -> Unit,
animationDelay: Int
) {
val surfaceColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
val borderColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0)
val textColor = if (isDarkTheme) Color.White else Color.Black
var visible by remember { mutableStateOf(false) }
@@ -299,62 +377,82 @@ private fun AddAccountCard(
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(400)) + scaleIn(
initialScale = 0.9f,
enter = fadeIn(tween(400)) + slideInHorizontally(
initialOffsetX = { -50 },
animationSpec = tween(400)
)
) {
Card(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(0.85f)
.height(64.dp)
.clickable(onClick = onClick),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = surfaceColor),
border = BorderStroke(
width = 2.dp,
color = borderColor,
)
border = BorderStroke(2.dp, PrimaryBlue)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
Row(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 20.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(20.dp)
) {
// Plus icon
Box(
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.border(2.dp, PrimaryBlue, CircleShape),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Add,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(40.dp)
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Add Account",
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = PrimaryBlue,
textAlign = TextAlign.Center
)
}
Icon(
Icons.Default.Add,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "Add New Account",
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = PrimaryBlue
)
}
}
}
}
@Composable
private fun EmptySearchResult(
isDarkTheme: Boolean,
searchQuery: String
) {
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 40.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
Icons.Default.Search,
contentDescription = null,
tint = secondaryTextColor,
modifier = Modifier.size(64.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "No accounts found",
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = textColor
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "No results for \"$searchQuery\"",
fontSize = 14.sp,
color = secondaryTextColor,
textAlign = TextAlign.Center
)
}
}
@Composable
private fun CreateAccountModal(
isDarkTheme: Boolean,