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