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:
@@ -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
|
||||
|
||||
@@ -292,9 +292,6 @@ class AuthStateManager(
|
||||
*/
|
||||
fun getCurrentAccount(): DecryptedAccountData? = currentDecryptedAccount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberAuthState(context: Context): AuthStateManager {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user