feat: Revamp UnlockScreen layout and enhance user experience with improved avatar display and dropdown account selection

This commit is contained in:
k1ngsterr1
2026-01-26 19:53:33 +05:00
parent 6b232006b0
commit 91eb8a4b63

View File

@@ -345,74 +345,31 @@ fun UnlockScreen(
Modifier.fillMaxSize() Modifier.fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.imePadding() .imePadding()
.padding(horizontal = 24.dp) .padding(horizontal = 32.dp)
.statusBarsPadding(), .statusBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Spacer(modifier = Modifier.height(40.dp)) Spacer(modifier = Modifier.height(60.dp))
// Rosetta Logo // 🔥 Большой аватар пользователя - главный элемент (как в desktop)
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = fadeIn(tween(300)) + scaleIn(tween(300, easing = FastOutSlowInEasing)) enter = fadeIn(tween(400)) + scaleIn(tween(400, easing = FastOutSlowInEasing))
) { ) {
Image( Box(
painter = painterResource(id = R.drawable.rosetta_icon), modifier = Modifier
contentDescription = "Rosetta", .size(120.dp)
modifier = Modifier.size(100.dp).clip(CircleShape) .clip(RoundedCornerShape(28.dp))
) .background(
if (selectedAccount != null) {
val colors = getAvatarColor(selectedAccount!!.publicKey, isDarkTheme)
colors.backgroundColor
} else {
cardBackground
} }
),
Spacer(modifier = Modifier.height(24.dp)) contentAlignment = Alignment.Center
// Title
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(400, delayMillis = 200))
) { ) {
Text(
text = "Welcome Back",
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
color = textColor
)
}
Spacer(modifier = Modifier.height(8.dp))
AnimatedVisibility(visible = visible, enter = fadeIn(tween(600, delayMillis = 300))) {
Text(
text = "Select your account and enter password",
fontSize = 16.sp,
color = secondaryTextColor,
textAlign = TextAlign.Center
)
}
Spacer(modifier = Modifier.height(32.dp))
// Account Selector Card
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(400, delayMillis = 350))
) {
Column {
// Account selector dropdown
Card(
modifier =
Modifier.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.clickable(enabled = accounts.size > 1) {
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) { if (selectedAccount != null) {
val database = RosettaDatabase.getDatabase(context) val database = RosettaDatabase.getDatabase(context)
val avatarRepository = remember(selectedAccount!!.publicKey) { val avatarRepository = remember(selectedAccount!!.publicKey) {
@@ -421,157 +378,98 @@ fun UnlockScreen(
AvatarImage( AvatarImage(
publicKey = selectedAccount!!.publicKey, publicKey = selectedAccount!!.publicKey,
avatarRepository = avatarRepository, avatarRepository = avatarRepository,
size = 48.dp, size = 120.dp,
isDarkTheme = isDarkTheme isDarkTheme = isDarkTheme
) )
} else {
Text(
text = "?",
fontSize = 48.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
}
}
} }
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.height(20.dp))
// Account info // 🔥 Имя пользователя с иконкой переключения (как в desktop)
Column(modifier = Modifier.weight(1f)) { AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(400, delayMillis = 150))
) {
Row(
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.then(
if (accounts.size > 1)
Modifier.clickable { isDropdownExpanded = !isDropdownExpanded }
else Modifier
)
.padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Text( Text(
text = selectedAccount?.name ?: "Select Account", text = selectedAccount?.name ?: "Select Account",
fontSize = 16.sp, fontSize = 24.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.Bold,
color = textColor, color = textColor,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
if (selectedAccount != null) {
val displayText = selectedAccount!!.encryptedAccount.username
?: selectedAccount!!.publicKey.take(20) + "..."
Text(
text = displayText,
fontSize = 13.sp,
color = secondaryTextColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
// Dropdown arrow with rotation (only show if multiple accounts) // Иконка переключения аккаунта (только если > 1 аккаунта)
if (accounts.size > 1) { if (accounts.size > 1) {
Spacer(modifier = Modifier.width(4.dp))
Icon( Icon(
imageVector = TablerIcons.ChevronDown, imageVector = TablerIcons.ChevronDown,
contentDescription = null, contentDescription = "Switch account",
tint = secondaryTextColor.copy(alpha = 0.6f), tint = PrimaryBlue,
modifier = modifier = Modifier.size(20.dp)
Modifier.size(24.dp).graphicsLayer {
rotationZ = 180f * dropdownProgress
}
) )
} }
} }
} }
// Dropdown list with animation Spacer(modifier = Modifier.height(8.dp))
// Подзаголовок
AnimatedVisibility(visible = visible, enter = fadeIn(tween(400, delayMillis = 200))) {
Text(
text = "Enter password to unlock",
fontSize = 15.sp,
color = secondaryTextColor,
textAlign = TextAlign.Center
)
}
Spacer(modifier = Modifier.height(40.dp))
// 🔥 Dropdown для выбора аккаунта (показывается только при клике на иконку)
AnimatedVisibility( AnimatedVisibility(
visible = isDropdownExpanded && accounts.size > 1, visible = isDropdownExpanded && accounts.size > 1,
enter = enter = fadeIn(tween(200)) + expandVertically(expandFrom = Alignment.Top),
fadeIn(tween(150)) + exit = fadeOut(tween(150)) + shrinkVertically(shrinkTowards = Alignment.Top)
expandVertically(
expandFrom = Alignment.Top,
animationSpec =
tween(200, easing = FastOutSlowInEasing)
),
exit =
fadeOut(tween(100)) +
shrinkVertically(
shrinkTowards = Alignment.Top,
animationSpec = tween(150)
)
) { ) {
Card( Card(
modifier = modifier = Modifier
Modifier.fillMaxWidth() .fillMaxWidth()
.padding(top = 8.dp) .padding(bottom = 20.dp)
.heightIn( .heightIn(max = if (accounts.size > 5) 300.dp else ((accounts.size * 64 + 16).dp)),
max =
if (accounts.size > 5) 350.dp
else ((accounts.size * 64 + 70).dp)
),
colors = CardDefaults.cardColors(containerColor = cardBackground), colors = CardDefaults.cardColors(containerColor = cardBackground),
shape = RoundedCornerShape(16.dp) shape = RoundedCornerShape(16.dp)
) { ) {
Column {
// Search field - only show if more than 3 accounts
if (accounts.size > 3) {
OutlinedTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
placeholder = {
Text(
"Search accounts...",
color =
secondaryTextColor.copy(
alpha = 0.6f
)
)
},
leadingIcon = {
Icon(
TablerIcons.Search,
contentDescription = null,
tint = secondaryTextColor.copy(alpha = 0.6f)
)
},
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( LazyColumn(
modifier = modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)
Modifier.fillMaxWidth()
.padding(
vertical =
if (accounts.size <= 3) 8.dp
else 0.dp
)
) { ) {
items(filteredAccounts, key = { it.publicKey }) { account -> items(accounts, key = { it.publicKey }) { account ->
val isSelected = val isSelected = account.publicKey == selectedAccount?.publicKey
account.publicKey == selectedAccount?.publicKey
val itemScale by
animateFloatAsState(
targetValue = if (isSelected) 1f else 0.98f,
label = "itemScale"
)
Row( Row(
modifier = modifier = Modifier
Modifier.fillMaxWidth() .fillMaxWidth()
.scale(itemScale)
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp))
.clickable { .clickable {
selectedAccount = account selectedAccount = account
@@ -580,19 +478,12 @@ fun UnlockScreen(
error = null error = null
} }
.background( .background(
if (isSelected) if (isSelected) PrimaryBlue.copy(alpha = 0.1f)
PrimaryBlue.copy(
alpha = 0.1f
)
else Color.Transparent else Color.Transparent
) )
.padding( .padding(horizontal = 16.dp, vertical = 12.dp),
horizontal = 16.dp,
vertical = 12.dp
),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Avatar
val database = RosettaDatabase.getDatabase(context) val database = RosettaDatabase.getDatabase(context)
val avatarRepository = remember(account.publicKey) { val avatarRepository = remember(account.publicKey) {
AvatarRepository(context, database.avatarDao(), account.publicKey) AvatarRepository(context, database.avatarDao(), account.publicKey)
@@ -610,9 +501,7 @@ fun UnlockScreen(
Text( Text(
text = account.name, text = account.name,
fontSize = 15.sp, fontSize = 15.sp,
fontWeight = fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
if (isSelected) FontWeight.SemiBold
else FontWeight.Normal,
color = textColor, color = textColor,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
@@ -638,32 +527,14 @@ fun UnlockScreen(
} }
} }
} }
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( AnimatedVisibility(
visible = visible && !isDropdownExpanded, visible = visible && !isDropdownExpanded,
enter = fadeIn(tween(400, delayMillis = 400)) enter = fadeIn(tween(400, delayMillis = 300))
) { ) {
OutlinedTextField( OutlinedTextField(
value = password, value = password,
@@ -671,8 +542,12 @@ fun UnlockScreen(
password = it password = it
error = null error = null
}, },
label = { Text("Password") }, placeholder = {
placeholder = { Text("Enter your password") }, Text(
"Password",
color = secondaryTextColor.copy(alpha = 0.6f)
)
},
singleLine = true, singleLine = true,
visualTransformation = visualTransformation =
if (passwordVisible) VisualTransformation.None if (passwordVisible) VisualTransformation.None
@@ -683,27 +558,26 @@ fun UnlockScreen(
imageVector = imageVector =
if (passwordVisible) TablerIcons.EyeOff if (passwordVisible) TablerIcons.EyeOff
else TablerIcons.Eye, else TablerIcons.Eye,
contentDescription = if (passwordVisible) "Hide" else "Show" contentDescription = if (passwordVisible) "Hide" else "Show",
tint = secondaryTextColor
) )
} }
}, },
isError = error != null, isError = error != null,
colors = colors = OutlinedTextFieldDefaults.colors(
OutlinedTextFieldDefaults.colors(
focusedBorderColor = PrimaryBlue, focusedBorderColor = PrimaryBlue,
unfocusedBorderColor = unfocusedBorderColor = Color.Transparent,
if (isDarkTheme) Color(0xFF4A4A4A) focusedContainerColor = cardBackground,
else Color(0xFFD0D0D0), unfocusedContainerColor = cardBackground,
focusedLabelColor = PrimaryBlue,
cursorColor = PrimaryBlue,
focusedTextColor = textColor, focusedTextColor = textColor,
unfocusedTextColor = textColor, unfocusedTextColor = textColor,
errorBorderColor = Color(0xFFE53935) cursorColor = PrimaryBlue,
errorBorderColor = Color(0xFFE53935),
errorContainerColor = cardBackground
), ),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(14.dp),
keyboardOptions = keyboardOptions = KeyboardOptions(
KeyboardOptions(
keyboardType = KeyboardType.Password, keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done imeAction = ImeAction.Done
) )
@@ -722,13 +596,11 @@ fun UnlockScreen(
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
// Unlock Button // 🔥 Градиентная кнопка Unlock
AnimatedVisibility( AnimatedVisibility(
visible = visible && !isDropdownExpanded, visible = visible && !isDropdownExpanded,
enter = fadeIn(tween(400, delayMillis = 500)) enter = fadeIn(tween(400, delayMillis = 400))
) { ) {
Column {
// Check if biometric unlock is available for selected account
val canUseBiometric = remember(selectedAccount, isBiometricEnabled, biometricAvailable) { val canUseBiometric = remember(selectedAccount, isBiometricEnabled, biometricAvailable) {
selectedAccount != null && selectedAccount != null &&
isBiometricEnabled && isBiometricEnabled &&
@@ -742,11 +614,8 @@ fun UnlockScreen(
val showBiometricButton = canUseBiometric && hasSavedPassword val showBiometricButton = canUseBiometric && hasSavedPassword
Row( Column(horizontalAlignment = Alignment.CenterHorizontally) {
modifier = Modifier.fillMaxWidth(), // Основная кнопка Unlock
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Password Unlock Button
Button( Button(
onClick = { onClick = {
if (selectedAccount == null) { if (selectedAccount == null) {
@@ -773,20 +642,19 @@ fun UnlockScreen(
}, },
enabled = selectedAccount != null && password.isNotEmpty() && !isUnlocking, enabled = selectedAccount != null && password.isNotEmpty() && !isUnlocking,
modifier = Modifier modifier = Modifier
.weight(1f) .fillMaxWidth()
.height(56.dp), .height(54.dp),
colors = colors = ButtonDefaults.buttonColors(
ButtonDefaults.buttonColors(
containerColor = PrimaryBlue, containerColor = PrimaryBlue,
contentColor = Color.White, contentColor = Color.White,
disabledContainerColor = PrimaryBlue.copy(alpha = 0.5f), disabledContainerColor = PrimaryBlue.copy(alpha = 0.4f),
disabledContentColor = Color.White.copy(alpha = 0.5f) disabledContentColor = Color.White.copy(alpha = 0.6f)
), ),
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(14.dp)
) { ) {
if (isUnlocking) { if (isUnlocking) {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.size(24.dp), modifier = Modifier.size(22.dp),
color = Color.White, color = Color.White,
strokeWidth = 2.dp strokeWidth = 2.dp
) )
@@ -796,62 +664,91 @@ fun UnlockScreen(
contentDescription = null, contentDescription = null,
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(10.dp))
Text(text = "Unlock", fontSize = 16.sp, fontWeight = FontWeight.SemiBold) Text(text = "Unlock", fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
} }
} }
// Biometric Unlock Button // Кнопка биометрии (если доступна)
if (showBiometricButton) { if (showBiometricButton) {
Button( Spacer(modifier = Modifier.height(16.dp))
onClick = {
tryBiometricUnlock() TextButton(
}, onClick = { tryBiometricUnlock() },
enabled = !isUnlocking, enabled = !isUnlocking
modifier = Modifier
.width(56.dp)
.height(56.dp),
colors =
ButtonDefaults.buttonColors(
containerColor = PrimaryBlue,
contentColor = Color.White,
disabledContainerColor = PrimaryBlue.copy(alpha = 0.5f),
disabledContentColor = Color.White.copy(alpha = 0.5f)
),
shape = RoundedCornerShape(12.dp),
contentPadding = PaddingValues(0.dp)
) { ) {
Icon( Icon(
imageVector = TablerIcons.Fingerprint, imageVector = TablerIcons.Fingerprint,
contentDescription = "Unlock with biometric", contentDescription = null,
modifier = Modifier.size(28.dp) tint = PrimaryBlue,
modifier = Modifier.size(22.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Use biometric",
color = PrimaryBlue,
fontSize = 15.sp,
fontWeight = FontWeight.Medium
) )
} }
} }
} }
} }
}
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.weight(1f))
// Create New Account button // 🔥 Footer - Create/Import account
AnimatedVisibility( AnimatedVisibility(
visible = visible && !isDropdownExpanded, visible = visible && !isDropdownExpanded,
enter = fadeIn(tween(400, delayMillis = 600)) enter = fadeIn(tween(400, delayMillis = 500))
) { ) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(bottom = 32.dp)
) {
// Разделитель
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.weight(1f)
.height(0.5.dp)
.background(secondaryTextColor.copy(alpha = 0.3f))
)
Text(
text = " or ",
color = secondaryTextColor,
fontSize = 13.sp
)
Box(
modifier = Modifier
.weight(1f)
.height(0.5.dp)
.background(secondaryTextColor.copy(alpha = 0.3f))
)
}
Spacer(modifier = Modifier.height(20.dp))
TextButton(onClick = onSwitchAccount) { TextButton(onClick = onSwitchAccount) {
Icon( Icon(
imageVector = TablerIcons.UserPlus, imageVector = TablerIcons.UserPlus,
contentDescription = null, contentDescription = null,
tint = PrimaryBlue, tint = PrimaryBlue,
modifier = Modifier.size(20.dp) modifier = Modifier.size(18.dp)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text(text = "Create New Account", color = PrimaryBlue, fontSize = 15.sp) Text(
} text = "Create or import account",
} color = PrimaryBlue,
fontSize = 15.sp,
Spacer(modifier = Modifier.height(60.dp)) fontWeight = FontWeight.Medium
)
}
}
}
} }
} }
} }