feat: Revamp UnlockScreen layout and enhance user experience with improved avatar display and dropdown account selection
This commit is contained in:
@@ -345,312 +345,185 @@ 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
|
||||||
|
}
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (selectedAccount != null) {
|
||||||
|
val database = RosettaDatabase.getDatabase(context)
|
||||||
|
val avatarRepository = remember(selectedAccount!!.publicKey) {
|
||||||
|
AvatarRepository(context, database.avatarDao(), selectedAccount!!.publicKey)
|
||||||
|
}
|
||||||
|
AvatarImage(
|
||||||
|
publicKey = selectedAccount!!.publicKey,
|
||||||
|
avatarRepository = avatarRepository,
|
||||||
|
size = 120.dp,
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "?",
|
||||||
|
fontSize = 48.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
// Title
|
// 🔥 Имя пользователя с иконкой переключения (как в desktop)
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn(tween(400, delayMillis = 200))
|
enter = fadeIn(tween(400, delayMillis = 150))
|
||||||
) {
|
) {
|
||||||
Text(
|
Row(
|
||||||
text = "Welcome Back",
|
modifier = Modifier
|
||||||
fontSize = 28.sp,
|
.clip(RoundedCornerShape(8.dp))
|
||||||
fontWeight = FontWeight.Bold,
|
.then(
|
||||||
color = textColor
|
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 = 24.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = textColor,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
|
// Иконка переключения аккаунта (только если > 1 аккаунта)
|
||||||
|
if (accounts.size > 1) {
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Icon(
|
||||||
|
imageVector = TablerIcons.ChevronDown,
|
||||||
|
contentDescription = "Switch account",
|
||||||
|
tint = PrimaryBlue,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
AnimatedVisibility(visible = visible, enter = fadeIn(tween(600, delayMillis = 300))) {
|
// Подзаголовок
|
||||||
|
AnimatedVisibility(visible = visible, enter = fadeIn(tween(400, delayMillis = 200))) {
|
||||||
Text(
|
Text(
|
||||||
text = "Select your account and enter password",
|
text = "Enter password to unlock",
|
||||||
fontSize = 16.sp,
|
fontSize = 15.sp,
|
||||||
color = secondaryTextColor,
|
color = secondaryTextColor,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
|
|
||||||
// Account Selector Card
|
// 🔥 Dropdown для выбора аккаунта (показывается только при клике на иконку)
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = isDropdownExpanded && accounts.size > 1,
|
||||||
enter = fadeIn(tween(400, delayMillis = 350))
|
enter = fadeIn(tween(200)) + expandVertically(expandFrom = Alignment.Top),
|
||||||
|
exit = fadeOut(tween(150)) + shrinkVertically(shrinkTowards = Alignment.Top)
|
||||||
) {
|
) {
|
||||||
Column {
|
Card(
|
||||||
// Account selector dropdown
|
modifier = Modifier
|
||||||
Card(
|
.fillMaxWidth()
|
||||||
modifier =
|
.padding(bottom = 20.dp)
|
||||||
Modifier.fillMaxWidth()
|
.heightIn(max = if (accounts.size > 5) 300.dp else ((accounts.size * 64 + 16).dp)),
|
||||||
.clip(RoundedCornerShape(16.dp))
|
colors = CardDefaults.cardColors(containerColor = cardBackground),
|
||||||
.clickable(enabled = accounts.size > 1) {
|
shape = RoundedCornerShape(16.dp)
|
||||||
isDropdownExpanded = !isDropdownExpanded
|
) {
|
||||||
},
|
LazyColumn(
|
||||||
colors = CardDefaults.cardColors(containerColor = cardBackground),
|
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)
|
||||||
shape = RoundedCornerShape(16.dp)
|
|
||||||
) {
|
) {
|
||||||
Row(
|
items(accounts, key = { it.publicKey }) { account ->
|
||||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
val isSelected = account.publicKey == selectedAccount?.publicKey
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
Row(
|
||||||
// Avatar
|
modifier = Modifier
|
||||||
if (selectedAccount != null) {
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.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
|
||||||
|
) {
|
||||||
val database = RosettaDatabase.getDatabase(context)
|
val database = RosettaDatabase.getDatabase(context)
|
||||||
val avatarRepository = remember(selectedAccount!!.publicKey) {
|
val avatarRepository = remember(account.publicKey) {
|
||||||
AvatarRepository(context, database.avatarDao(), selectedAccount!!.publicKey)
|
AvatarRepository(context, database.avatarDao(), account.publicKey)
|
||||||
}
|
}
|
||||||
AvatarImage(
|
AvatarImage(
|
||||||
publicKey = selectedAccount!!.publicKey,
|
publicKey = account.publicKey,
|
||||||
avatarRepository = avatarRepository,
|
avatarRepository = avatarRepository,
|
||||||
size = 48.dp,
|
size = 40.dp,
|
||||||
isDarkTheme = isDarkTheme
|
isDarkTheme = isDarkTheme
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
// Account info
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Text(
|
||||||
Text(
|
text = account.name,
|
||||||
text = selectedAccount?.name ?: "Select Account",
|
fontSize = 15.sp,
|
||||||
fontSize = 16.sp,
|
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
|
||||||
fontWeight = FontWeight.SemiBold,
|
color = textColor,
|
||||||
color = textColor,
|
maxLines = 1,
|
||||||
maxLines = 1,
|
overflow = TextOverflow.Ellipsis
|
||||||
overflow = TextOverflow.Ellipsis
|
)
|
||||||
)
|
val displayText = account.encryptedAccount.username
|
||||||
if (selectedAccount != null) {
|
?: account.publicKey.take(16) + "..."
|
||||||
val displayText = selectedAccount!!.encryptedAccount.username
|
|
||||||
?: selectedAccount!!.publicKey.take(20) + "..."
|
|
||||||
Text(
|
Text(
|
||||||
text = displayText,
|
text = displayText,
|
||||||
fontSize = 13.sp,
|
fontSize = 12.sp,
|
||||||
color = secondaryTextColor,
|
color = secondaryTextColor,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Dropdown arrow with rotation (only show if multiple accounts)
|
if (isSelected) {
|
||||||
if (accounts.size > 1) {
|
Icon(
|
||||||
Icon(
|
TablerIcons.Check,
|
||||||
imageVector = TablerIcons.ChevronDown,
|
contentDescription = null,
|
||||||
contentDescription = null,
|
tint = PrimaryBlue,
|
||||||
tint = secondaryTextColor.copy(alpha = 0.6f),
|
modifier = Modifier.size(20.dp)
|
||||||
modifier =
|
|
||||||
Modifier.size(24.dp).graphicsLayer {
|
|
||||||
rotationZ = 180f * dropdownProgress
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dropdown list with animation
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier =
|
|
||||||
Modifier.fillMaxWidth()
|
|
||||||
.padding(top = 8.dp)
|
|
||||||
.heightIn(
|
|
||||||
max =
|
|
||||||
if (accounts.size > 5) 350.dp
|
|
||||||
else ((accounts.size * 64 + 70).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
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
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)
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.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 database = RosettaDatabase.getDatabase(context)
|
|
||||||
val avatarRepository = remember(account.publicKey) {
|
|
||||||
AvatarRepository(context, database.avatarDao(), account.publicKey)
|
|
||||||
}
|
|
||||||
AvatarImage(
|
|
||||||
publicKey = account.publicKey,
|
|
||||||
avatarRepository = avatarRepository,
|
|
||||||
size = 40.dp,
|
|
||||||
isDarkTheme = isDarkTheme
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
val displayText = account.encryptedAccount.username
|
|
||||||
?: account.publicKey.take(16) + "..."
|
|
||||||
Text(
|
|
||||||
text = displayText,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = secondaryTextColor,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSelected) {
|
|
||||||
Icon(
|
|
||||||
TablerIcons.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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -658,12 +531,10 @@ fun UnlockScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,30 +558,29 @@ 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 = Color.Transparent,
|
||||||
unfocusedBorderColor =
|
focusedContainerColor = cardBackground,
|
||||||
if (isDarkTheme) Color(0xFF4A4A4A)
|
unfocusedContainerColor = cardBackground,
|
||||||
else Color(0xFFD0D0D0),
|
focusedTextColor = textColor,
|
||||||
focusedLabelColor = PrimaryBlue,
|
unfocusedTextColor = textColor,
|
||||||
cursorColor = PrimaryBlue,
|
cursorColor = PrimaryBlue,
|
||||||
focusedTextColor = textColor,
|
errorBorderColor = Color(0xFFE53935),
|
||||||
unfocusedTextColor = textColor,
|
errorContainerColor = cardBackground
|
||||||
errorBorderColor = Color(0xFFE53935)
|
),
|
||||||
),
|
|
||||||
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,136 +596,159 @@ fun UnlockScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// Unlock Button
|
// 🔥 Градиентная кнопка Unlock
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible && !isDropdownExpanded,
|
||||||
|
enter = fadeIn(tween(400, delayMillis = 400))
|
||||||
|
) {
|
||||||
|
val canUseBiometric = remember(selectedAccount, isBiometricEnabled, biometricAvailable) {
|
||||||
|
selectedAccount != null &&
|
||||||
|
isBiometricEnabled &&
|
||||||
|
biometricAvailable is BiometricAvailability.Available &&
|
||||||
|
activity != null
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasSavedPassword = remember(selectedAccount, savedPasswordsMap) {
|
||||||
|
selectedAccount?.let { savedPasswordsMap.containsKey(it.publicKey) } ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
val showBiometricButton = canUseBiometric && hasSavedPassword
|
||||||
|
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
// Основная кнопка Unlock
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (selectedAccount == null) {
|
||||||
|
error = "Please select an account"
|
||||||
|
return@Button
|
||||||
|
}
|
||||||
|
if (password.isEmpty()) {
|
||||||
|
error = "Please enter your password"
|
||||||
|
return@Button
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
performUnlock(
|
||||||
|
selectedAccount = selectedAccount,
|
||||||
|
password = password,
|
||||||
|
accountManager = accountManager,
|
||||||
|
onUnlocking = { isUnlocking = it },
|
||||||
|
onError = { error = it },
|
||||||
|
onSuccess = { decryptedAccount ->
|
||||||
|
onUnlocked(decryptedAccount)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = selectedAccount != null && password.isNotEmpty() && !isUnlocking,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(54.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = PrimaryBlue,
|
||||||
|
contentColor = Color.White,
|
||||||
|
disabledContainerColor = PrimaryBlue.copy(alpha = 0.4f),
|
||||||
|
disabledContentColor = Color.White.copy(alpha = 0.6f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(14.dp)
|
||||||
|
) {
|
||||||
|
if (isUnlocking) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(22.dp),
|
||||||
|
color = Color.White,
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = TablerIcons.LockOpen,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
Text(text = "Unlock", fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка биометрии (если доступна)
|
||||||
|
if (showBiometricButton) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
onClick = { tryBiometricUnlock() },
|
||||||
|
enabled = !isUnlocking
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = TablerIcons.Fingerprint,
|
||||||
|
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.weight(1f))
|
||||||
|
|
||||||
|
// 🔥 Footer - Create/Import account
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible && !isDropdownExpanded,
|
visible = visible && !isDropdownExpanded,
|
||||||
enter = fadeIn(tween(400, delayMillis = 500))
|
enter = fadeIn(tween(400, delayMillis = 500))
|
||||||
) {
|
) {
|
||||||
Column {
|
Column(
|
||||||
// Check if biometric unlock is available for selected account
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
val canUseBiometric = remember(selectedAccount, isBiometricEnabled, biometricAvailable) {
|
modifier = Modifier.padding(bottom = 32.dp)
|
||||||
selectedAccount != null &&
|
) {
|
||||||
isBiometricEnabled &&
|
// Разделитель
|
||||||
biometricAvailable is BiometricAvailability.Available &&
|
|
||||||
activity != null
|
|
||||||
}
|
|
||||||
|
|
||||||
val hasSavedPassword = remember(selectedAccount, savedPasswordsMap) {
|
|
||||||
selectedAccount?.let { savedPasswordsMap.containsKey(it.publicKey) } ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
val showBiometricButton = canUseBiometric && hasSavedPassword
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// Password Unlock Button
|
Box(
|
||||||
Button(
|
modifier = Modifier
|
||||||
onClick = {
|
.weight(1f)
|
||||||
if (selectedAccount == null) {
|
.height(0.5.dp)
|
||||||
error = "Please select an account"
|
.background(secondaryTextColor.copy(alpha = 0.3f))
|
||||||
return@Button
|
)
|
||||||
}
|
Text(
|
||||||
if (password.isEmpty()) {
|
text = " or ",
|
||||||
error = "Please enter your password"
|
color = secondaryTextColor,
|
||||||
return@Button
|
fontSize = 13.sp
|
||||||
}
|
)
|
||||||
|
Box(
|
||||||
scope.launch {
|
modifier = Modifier
|
||||||
performUnlock(
|
.weight(1f)
|
||||||
selectedAccount = selectedAccount,
|
.height(0.5.dp)
|
||||||
password = password,
|
.background(secondaryTextColor.copy(alpha = 0.3f))
|
||||||
accountManager = accountManager,
|
)
|
||||||
onUnlocking = { isUnlocking = it },
|
}
|
||||||
onError = { error = it },
|
|
||||||
onSuccess = { decryptedAccount ->
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
onUnlocked(decryptedAccount)
|
|
||||||
}
|
TextButton(onClick = onSwitchAccount) {
|
||||||
)
|
Icon(
|
||||||
}
|
imageVector = TablerIcons.UserPlus,
|
||||||
},
|
contentDescription = null,
|
||||||
enabled = selectedAccount != null && password.isNotEmpty() && !isUnlocking,
|
tint = PrimaryBlue,
|
||||||
modifier = Modifier
|
modifier = Modifier.size(18.dp)
|
||||||
.weight(1f)
|
)
|
||||||
.height(56.dp),
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
colors =
|
Text(
|
||||||
ButtonDefaults.buttonColors(
|
text = "Create or import account",
|
||||||
containerColor = PrimaryBlue,
|
color = PrimaryBlue,
|
||||||
contentColor = Color.White,
|
fontSize = 15.sp,
|
||||||
disabledContainerColor = PrimaryBlue.copy(alpha = 0.5f),
|
fontWeight = FontWeight.Medium
|
||||||
disabledContentColor = Color.White.copy(alpha = 0.5f)
|
)
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
) {
|
|
||||||
if (isUnlocking) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
color = Color.White,
|
|
||||||
strokeWidth = 2.dp
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Icon(
|
|
||||||
imageVector = TablerIcons.LockOpen,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.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)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = TablerIcons.Fingerprint,
|
|
||||||
contentDescription = "Unlock with biometric",
|
|
||||||
modifier = Modifier.size(28.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
// Create New Account button
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = visible && !isDropdownExpanded,
|
|
||||||
enter = fadeIn(tween(400, delayMillis = 600))
|
|
||||||
) {
|
|
||||||
TextButton(onClick = onSwitchAccount) {
|
|
||||||
Icon(
|
|
||||||
imageVector = TablerIcons.UserPlus,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = PrimaryBlue,
|
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(text = "Create New Account", color = PrimaryBlue, fontSize = 15.sp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(60.dp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user