feat: Enhance UnlockScreen with account selection dropdown and search functionality
This commit is contained in:
@@ -201,7 +201,7 @@ fun MainScreen(
|
||||
onToggleTheme: () -> Unit = {},
|
||||
onLogout: () -> Unit = {}
|
||||
) {
|
||||
val accountName = account?.name ?: "Rosetta User"
|
||||
val accountName = account?.publicKey ?: "04c266b98ae5"
|
||||
val accountPhone = account?.publicKey?.take(16)?.let {
|
||||
"+${it.take(1)} ${it.substring(1, 4)} ${it.substring(4, 7)}${it.substring(7)}"
|
||||
} ?: "+7 775 9932587"
|
||||
|
||||
@@ -130,9 +130,9 @@ fun AuthFlow(
|
||||
selectedAccountId = selectedAccountId,
|
||||
onUnlocked = { account -> onAuthComplete(account) },
|
||||
onSwitchAccount = {
|
||||
if (accounts.size > 1) {
|
||||
currentScreen = AuthScreen.SELECT_ACCOUNT
|
||||
}
|
||||
// Navigate to create new account screen
|
||||
currentScreen = AuthScreen.WELCOME
|
||||
selectedAccountId = null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -58,6 +58,24 @@ fun SetPasswordScreen(
|
||||
var error by remember { mutableStateOf<String?>(null) }
|
||||
var visible by remember { mutableStateOf(false) }
|
||||
|
||||
// Track keyboard visibility
|
||||
val view = androidx.compose.ui.platform.LocalView.current
|
||||
var isKeyboardVisible by remember { mutableStateOf(false) }
|
||||
|
||||
DisposableEffect(view) {
|
||||
val listener = android.view.ViewTreeObserver.OnGlobalLayoutListener {
|
||||
val rect = android.graphics.Rect()
|
||||
view.getWindowVisibleDisplayFrame(rect)
|
||||
val screenHeight = view.rootView.height
|
||||
val keypadHeight = screenHeight - rect.bottom
|
||||
isKeyboardVisible = keypadHeight > screenHeight * 0.15
|
||||
}
|
||||
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
|
||||
onDispose {
|
||||
view.viewTreeObserver.removeOnGlobalLayoutListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
visible = true
|
||||
}
|
||||
@@ -109,9 +127,18 @@ fun SetPasswordScreen(
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 8.dp else 16.dp))
|
||||
|
||||
// Lock Icon - smaller when keyboard is visible
|
||||
val iconSize by animateDpAsState(
|
||||
targetValue = if (isKeyboardVisible) 48.dp else 80.dp,
|
||||
animationSpec = tween(300, easing = FastOutSlowInEasing)
|
||||
)
|
||||
val iconInnerSize by animateDpAsState(
|
||||
targetValue = if (isKeyboardVisible) 24.dp else 40.dp,
|
||||
animationSpec = tween(300, easing = FastOutSlowInEasing)
|
||||
)
|
||||
|
||||
// Lock Icon
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(500)) + scaleIn(
|
||||
@@ -121,8 +148,8 @@ fun SetPasswordScreen(
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.size(iconSize)
|
||||
.clip(RoundedCornerShape(if (isKeyboardVisible) 12.dp else 20.dp))
|
||||
.background(PrimaryBlue.copy(alpha = 0.1f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
@@ -130,12 +157,12 @@ fun SetPasswordScreen(
|
||||
Icons.Default.Lock,
|
||||
contentDescription = null,
|
||||
tint = PrimaryBlue,
|
||||
modifier = Modifier.size(40.dp)
|
||||
modifier = Modifier.size(iconInnerSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 12.dp else 24.dp))
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
@@ -146,13 +173,13 @@ fun SetPasswordScreen(
|
||||
) {
|
||||
Text(
|
||||
text = "Protect Your Account",
|
||||
fontSize = 24.sp,
|
||||
fontSize = if (isKeyboardVisible) 20.sp else 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = textColor
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 6.dp else 8.dp))
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
@@ -160,14 +187,14 @@ fun SetPasswordScreen(
|
||||
) {
|
||||
Text(
|
||||
text = "This password encrypts your keys locally.\nYou'll need it to unlock Rosetta.",
|
||||
fontSize = 14.sp,
|
||||
fontSize = if (isKeyboardVisible) 12.sp else 14.sp,
|
||||
color = secondaryTextColor,
|
||||
textAlign = TextAlign.Center,
|
||||
lineHeight = 20.sp
|
||||
lineHeight = if (isKeyboardVisible) 16.sp else 20.sp
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 16.dp else 32.dp))
|
||||
|
||||
// Password Field
|
||||
AnimatedVisibility(
|
||||
|
||||
@@ -5,6 +5,8 @@ import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.foundation.text.KeyboardOptions
|
||||
@@ -15,7 +17,11 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -24,18 +30,28 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.rosetta.messenger.R
|
||||
import com.rosetta.messenger.crypto.CryptoManager
|
||||
import com.rosetta.messenger.data.AccountManager
|
||||
import com.rosetta.messenger.data.DecryptedAccount
|
||||
import com.rosetta.messenger.data.EncryptedAccount
|
||||
import com.rosetta.messenger.network.ProtocolManager
|
||||
import com.rosetta.messenger.network.ProtocolState
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
import com.rosetta.messenger.ui.chats.getAvatarColor
|
||||
import com.rosetta.messenger.ui.chats.getAvatarText
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
// Account model for dropdown
|
||||
data class AccountItem(
|
||||
val publicKey: String,
|
||||
val name: String,
|
||||
val encryptedAccount: EncryptedAccount
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun UnlockScreen(
|
||||
@@ -48,6 +64,7 @@ fun UnlockScreen(
|
||||
val backgroundColor by animateColorAsState(if (isDarkTheme) AuthBackground else AuthBackgroundLight, animationSpec = themeAnimSpec)
|
||||
val textColor by animateColorAsState(if (isDarkTheme) Color.White else Color.Black, animationSpec = themeAnimSpec)
|
||||
val secondaryTextColor by animateColorAsState(if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666), animationSpec = themeAnimSpec)
|
||||
val cardBackground by animateColorAsState(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5), animationSpec = themeAnimSpec)
|
||||
|
||||
val context = LocalContext.current
|
||||
val accountManager = remember { AccountManager(context) }
|
||||
@@ -57,17 +74,63 @@ fun UnlockScreen(
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
var isUnlocking by remember { mutableStateOf(false) }
|
||||
var error by remember { mutableStateOf<String?>(null) }
|
||||
var currentPublicKey by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
// Load current account
|
||||
// Account selection state
|
||||
var accounts by remember { mutableStateOf<List<AccountItem>>(emptyList()) }
|
||||
var selectedAccount by remember { mutableStateOf<AccountItem?>(null) }
|
||||
var isDropdownExpanded by remember { mutableStateOf(false) }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
val searchFocusRequester = remember { FocusRequester() }
|
||||
|
||||
// Load accounts
|
||||
LaunchedEffect(Unit) {
|
||||
currentPublicKey = selectedAccountId ?: accountManager.currentPublicKey.first()
|
||||
val allAccounts = accountManager.getAllAccounts()
|
||||
accounts = allAccounts.map { acc ->
|
||||
AccountItem(
|
||||
publicKey = acc.publicKey,
|
||||
name = acc.name,
|
||||
encryptedAccount = acc
|
||||
)
|
||||
}
|
||||
|
||||
// Select account
|
||||
val targetPublicKey = selectedAccountId ?: accountManager.currentPublicKey.first()
|
||||
selectedAccount = accounts.find { it.publicKey == targetPublicKey } ?: accounts.firstOrNull()
|
||||
}
|
||||
|
||||
// Filter accounts by search
|
||||
val filteredAccounts = remember(searchQuery, accounts) {
|
||||
if (searchQuery.isEmpty()) accounts
|
||||
else accounts.filter {
|
||||
it.name.contains(searchQuery, ignoreCase = true) ||
|
||||
it.publicKey.contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
// Entry animation
|
||||
var visible by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) { visible = true }
|
||||
|
||||
// Dropdown animation
|
||||
val dropdownProgress by animateFloatAsState(
|
||||
targetValue = if (isDropdownExpanded) 1f else 0f,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
),
|
||||
label = "dropdownProgress"
|
||||
)
|
||||
|
||||
// Auto-focus search when dropdown opens
|
||||
LaunchedEffect(isDropdownExpanded) {
|
||||
if (isDropdownExpanded) {
|
||||
kotlinx.coroutines.delay(200)
|
||||
searchFocusRequester.requestFocus()
|
||||
} else {
|
||||
searchQuery = ""
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -76,12 +139,13 @@ fun UnlockScreen(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.imePadding()
|
||||
.padding(horizontal = 24.dp)
|
||||
.statusBarsPadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(0.2f))
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
|
||||
// Rosetta Logo
|
||||
AnimatedVisibility(
|
||||
@@ -92,12 +156,12 @@ fun UnlockScreen(
|
||||
painter = painterResource(id = R.drawable.rosetta_icon),
|
||||
contentDescription = "Rosetta",
|
||||
modifier = Modifier
|
||||
.size(120.dp)
|
||||
.size(100.dp)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Title
|
||||
AnimatedVisibility(
|
||||
@@ -117,24 +181,263 @@ fun UnlockScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Account info
|
||||
AnimatedVisibility(
|
||||
visible = visible && currentPublicKey != null,
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(600, delayMillis = 300))
|
||||
) {
|
||||
Text(
|
||||
text = "Enter your password to unlock",
|
||||
text = "Select your account and enter password",
|
||||
fontSize = 16.sp,
|
||||
color = secondaryTextColor,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Account Selector Card
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(600, delayMillis = 350)) + slideInVertically(
|
||||
initialOffsetY = { 50 },
|
||||
animationSpec = tween(600, delayMillis = 350)
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
// Account selector dropdown
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { isDropdownExpanded = !isDropdownExpanded },
|
||||
colors = CardDefaults.cardColors(containerColor = cardBackground),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Avatar
|
||||
if (selectedAccount != null) {
|
||||
val avatarColors = getAvatarColor(selectedAccount!!.name, isDarkTheme)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.background(avatarColors.backgroundColor),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = getAvatarText(selectedAccount!!.publicKey),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = avatarColors.textColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
// Account info
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = selectedAccount?.name ?: "Select Account",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (selectedAccount != null) {
|
||||
Text(
|
||||
text = selectedAccount!!.publicKey.take(20) + "...",
|
||||
fontSize = 13.sp,
|
||||
color = secondaryTextColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown arrow with rotation
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = null,
|
||||
tint = secondaryTextColor,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.graphicsLayer {
|
||||
rotationZ = 180f * dropdownProgress
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown list with animation
|
||||
AnimatedVisibility(
|
||||
visible = isDropdownExpanded,
|
||||
enter = fadeIn(tween(200)) + expandVertically(
|
||||
expandFrom = Alignment.Top,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessMediumLow
|
||||
)
|
||||
),
|
||||
exit = fadeOut(tween(150)) + shrinkVertically(
|
||||
shrinkTowards = Alignment.Top,
|
||||
animationSpec = tween(200)
|
||||
)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
.heightIn(max = 300.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = cardBackground),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Column {
|
||||
// Search field
|
||||
OutlinedTextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
"Search accounts...",
|
||||
color = secondaryTextColor.copy(alpha = 0.6f)
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Default.Search,
|
||||
contentDescription = null,
|
||||
tint = secondaryTextColor
|
||||
)
|
||||
},
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = PrimaryBlue,
|
||||
unfocusedBorderColor = Color.Transparent,
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedTextColor = textColor,
|
||||
unfocusedTextColor = textColor,
|
||||
cursorColor = PrimaryBlue
|
||||
),
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
.focusRequester(searchFocusRequester),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
Divider(
|
||||
color = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0),
|
||||
thickness = 0.5.dp
|
||||
)
|
||||
|
||||
// Account list
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
items(filteredAccounts, key = { it.publicKey }) { account ->
|
||||
val isSelected = account.publicKey == selectedAccount?.publicKey
|
||||
val itemScale by animateFloatAsState(
|
||||
targetValue = if (isSelected) 1f else 0.98f,
|
||||
label = "itemScale"
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.scale(itemScale)
|
||||
.clickable {
|
||||
selectedAccount = account
|
||||
isDropdownExpanded = false
|
||||
password = ""
|
||||
error = null
|
||||
}
|
||||
.background(
|
||||
if (isSelected) PrimaryBlue.copy(alpha = 0.1f)
|
||||
else Color.Transparent
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Avatar
|
||||
val avatarColors = getAvatarColor(account.name, isDarkTheme)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.background(avatarColors.backgroundColor),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = getAvatarText(account.publicKey),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = avatarColors.textColor
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = account.name,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = account.publicKey.take(16) + "...",
|
||||
fontSize = 12.sp,
|
||||
color = secondaryTextColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
tint = PrimaryBlue,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredAccounts.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = "No accounts found",
|
||||
color = secondaryTextColor,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
// Password Field
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
visible = visible && !isDropdownExpanded,
|
||||
enter = fadeIn(tween(600, delayMillis = 400)) + slideInVertically(
|
||||
initialOffsetY = { 50 },
|
||||
animationSpec = tween(600, delayMillis = 400)
|
||||
@@ -197,7 +500,7 @@ fun UnlockScreen(
|
||||
|
||||
// Unlock Button
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
visible = visible && !isDropdownExpanded,
|
||||
enter = fadeIn(tween(600, delayMillis = 500)) + slideInVertically(
|
||||
initialOffsetY = { 50 },
|
||||
animationSpec = tween(600, delayMillis = 500)
|
||||
@@ -205,6 +508,10 @@ fun UnlockScreen(
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
if (selectedAccount == null) {
|
||||
error = "Please select an account"
|
||||
return@Button
|
||||
}
|
||||
if (password.isEmpty()) {
|
||||
error = "Please enter your password"
|
||||
return@Button
|
||||
@@ -213,20 +520,7 @@ fun UnlockScreen(
|
||||
isUnlocking = true
|
||||
scope.launch {
|
||||
try {
|
||||
val publicKey = currentPublicKey ?: run {
|
||||
error = "No account found"
|
||||
isUnlocking = false
|
||||
return@launch
|
||||
}
|
||||
|
||||
val accounts = accountManager.getAllAccounts()
|
||||
val account = accounts.find { it.publicKey == publicKey }
|
||||
|
||||
if (account == null) {
|
||||
error = "Account not found"
|
||||
isUnlocking = false
|
||||
return@launch
|
||||
}
|
||||
val account = selectedAccount!!.encryptedAccount
|
||||
|
||||
// Try to decrypt
|
||||
val decryptedPrivateKey = CryptoManager.decryptWithPassword(
|
||||
@@ -253,20 +547,20 @@ fun UnlockScreen(
|
||||
name = account.name
|
||||
)
|
||||
|
||||
// 🔌 Connect to server and authenticate
|
||||
Log.d("UnlockScreen", "🔌 Connecting to server...")
|
||||
// Connect to server and authenticate
|
||||
Log.d("UnlockScreen", "Connecting to server...")
|
||||
ProtocolManager.authenticate(account.publicKey, privateKeyHash)
|
||||
|
||||
accountManager.setCurrentAccount(publicKey)
|
||||
accountManager.setCurrentAccount(account.publicKey)
|
||||
onUnlocked(decryptedAccount)
|
||||
|
||||
} catch (e: Exception) {
|
||||
error = "Failed to unlock: ${e.message}"
|
||||
error = "Failed to unlock: \${e.message}"
|
||||
isUnlocking = false
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = password.isNotEmpty() && !isUnlocking,
|
||||
enabled = selectedAccount != null && password.isNotEmpty() && !isUnlocking,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
@@ -302,9 +596,9 @@ fun UnlockScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Switch Account button
|
||||
// Create New Account button
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
visible = visible && !isDropdownExpanded,
|
||||
enter = fadeIn(tween(600, delayMillis = 600)) + slideInVertically(
|
||||
initialOffsetY = { 50 },
|
||||
animationSpec = tween(600, delayMillis = 600)
|
||||
@@ -314,21 +608,21 @@ fun UnlockScreen(
|
||||
onClick = onSwitchAccount
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SwapHoriz,
|
||||
imageVector = Icons.Default.PersonAdd,
|
||||
contentDescription = null,
|
||||
tint = PrimaryBlue,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Switch Account",
|
||||
text = "Create New Account",
|
||||
color = PrimaryBlue,
|
||||
fontSize = 15.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(0.3f))
|
||||
Spacer(modifier = Modifier.height(60.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,73 +148,30 @@ fun ChatsListScreen(
|
||||
onNewChat: () -> Unit,
|
||||
onLogout: () -> Unit
|
||||
) {
|
||||
// Theme transition animation state
|
||||
var isTransitioning by remember { mutableStateOf(false) }
|
||||
var transitionProgress by remember { mutableStateOf(0f) }
|
||||
var clickPosition by remember { mutableStateOf(Offset.Zero) }
|
||||
var shouldUpdateStatusBar by remember { mutableStateOf(false) }
|
||||
// Theme transition state
|
||||
var hasInitialized by remember { mutableStateOf(false) }
|
||||
var previousTheme by remember { mutableStateOf(isDarkTheme) }
|
||||
var targetTheme by remember { mutableStateOf(isDarkTheme) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
hasInitialized = true
|
||||
}
|
||||
|
||||
// Theme transition animation
|
||||
LaunchedEffect(isTransitioning) {
|
||||
if (isTransitioning) {
|
||||
shouldUpdateStatusBar = false
|
||||
val duration = 800f
|
||||
val startTime = System.currentTimeMillis()
|
||||
while (transitionProgress < 1f) {
|
||||
val elapsed = System.currentTimeMillis() - startTime
|
||||
transitionProgress = (elapsed / duration).coerceAtMost(1f)
|
||||
kotlinx.coroutines.delay(16)
|
||||
}
|
||||
shouldUpdateStatusBar = true
|
||||
kotlinx.coroutines.delay(50)
|
||||
isTransitioning = false
|
||||
transitionProgress = 0f
|
||||
shouldUpdateStatusBar = false
|
||||
previousTheme = targetTheme
|
||||
}
|
||||
}
|
||||
|
||||
val view = androidx.compose.ui.platform.LocalView.current
|
||||
|
||||
// Animate navigation bar color starting at 80% of wave animation
|
||||
LaunchedEffect(isTransitioning, transitionProgress) {
|
||||
if (isTransitioning && transitionProgress >= 0.8f && !view.isInEditMode) {
|
||||
val window = (view.context as android.app.Activity).window
|
||||
val navProgress = ((transitionProgress - 0.8f) / 0.2f).coerceIn(0f, 1f)
|
||||
|
||||
val oldColor = if (previousTheme) 0xFF1A1A1A else 0xFFFFFFFF
|
||||
val newColor = if (targetTheme) 0xFF1A1A1A 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) {
|
||||
// Update status bar and navigation bar
|
||||
LaunchedEffect(isDarkTheme, drawerState.isOpen) {
|
||||
if (!view.isInEditMode) {
|
||||
val window = (view.context as android.app.Activity).window
|
||||
val insetsController = androidx.core.view.WindowCompat.getInsetsController(window, view)
|
||||
insetsController.isAppearanceLightStatusBars = !isDarkTheme
|
||||
insetsController.isAppearanceLightNavigationBars = !isDarkTheme
|
||||
window.statusBarColor = android.graphics.Color.TRANSPARENT
|
||||
// When drawer is open, dim the navigation bar to match overlay
|
||||
window.navigationBarColor = if (drawerState.isOpen) {
|
||||
// Darker color to match scrim overlay
|
||||
if (isDarkTheme) 0xFF0D0D0D.toInt() else 0xFF999999.toInt()
|
||||
} else {
|
||||
if (isDarkTheme) 0xFF1A1A1A.toInt() else 0xFFFFFFFF.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,29 +328,12 @@ fun ChatsListScreen(
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
// Base background - shows the OLD theme color during transition
|
||||
// Simple background
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(if (isTransitioning) {
|
||||
if (previousTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
|
||||
} else backgroundColor)
|
||||
.background(backgroundColor)
|
||||
)
|
||||
|
||||
// Circular reveal overlay - draws the NEW theme color expanding
|
||||
if (isTransitioning) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val maxRadius = kotlin.math.hypot(size.width, size.height)
|
||||
val radius = maxRadius * transitionProgress
|
||||
|
||||
// Draw the NEW theme color expanding from click point
|
||||
drawCircle(
|
||||
color = if (targetTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF),
|
||||
radius = radius,
|
||||
center = clickPosition
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
@@ -406,8 +346,9 @@ fun ChatsListScreen(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5))
|
||||
.padding(16.dp)
|
||||
.background(drawerBackgroundColor)
|
||||
.padding(top = 48.dp)
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
@@ -434,46 +375,25 @@ fun ChatsListScreen(
|
||||
|
||||
// Theme toggle
|
||||
IconButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.onGloballyPositioned { coordinates ->
|
||||
// This will be handled by clickable below
|
||||
}
|
||||
onClick = onToggleTheme
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
if (!isTransitioning) {
|
||||
previousTheme = isDarkTheme
|
||||
targetTheme = !isDarkTheme
|
||||
// Use center of icon as click position
|
||||
val screenWidth = view.width.toFloat()
|
||||
val screenHeight = view.height.toFloat()
|
||||
clickPosition = Offset(
|
||||
screenWidth - 48.dp.value * view.resources.displayMetrics.density,
|
||||
96.dp.value * view.resources.displayMetrics.density
|
||||
)
|
||||
isTransitioning = true
|
||||
onToggleTheme()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
if (isDarkTheme) Icons.Default.LightMode else Icons.Default.DarkMode,
|
||||
contentDescription = "Toggle theme",
|
||||
tint = textColor
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
if (isDarkTheme) Icons.Default.LightMode else Icons.Default.DarkMode,
|
||||
contentDescription = "Toggle theme",
|
||||
tint = textColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Account name
|
||||
// Public key instead of account name
|
||||
Text(
|
||||
text = accountName,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = textColor
|
||||
text = accountPublicKey,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = secondaryTextColor,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -508,6 +428,8 @@ fun ChatsListScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Logout button at bottom
|
||||
Divider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
@@ -545,6 +467,7 @@ fun ChatsListScreen(
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user