diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt index bbda92b..a35aa86 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt @@ -345,312 +345,185 @@ 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 + } + ), + 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( visible = visible, - enter = fadeIn(tween(400, delayMillis = 200)) + enter = fadeIn(tween(400, delayMillis = 150)) ) { - Text( - text = "Welcome Back", - fontSize = 28.sp, - fontWeight = FontWeight.Bold, - color = textColor - ) + 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 = 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)) - AnimatedVisibility(visible = visible, enter = fadeIn(tween(600, delayMillis = 300))) { + // Подзаголовок + AnimatedVisibility(visible = visible, enter = fadeIn(tween(400, delayMillis = 200))) { Text( - text = "Select your account and enter password", - fontSize = 16.sp, + text = "Enter password to unlock", + fontSize = 15.sp, color = secondaryTextColor, textAlign = TextAlign.Center ) } - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(40.dp)) - // Account Selector Card + // πŸ”₯ Dropdown для Π²Ρ‹Π±ΠΎΡ€Π° Π°ΠΊΠΊΠ°ΡƒΠ½Ρ‚Π° (показываСтся Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΏΡ€ΠΈ ΠΊΠ»ΠΈΠΊΠ΅ Π½Π° ΠΈΠΊΠΎΠ½ΠΊΡƒ) AnimatedVisibility( - visible = visible, - enter = fadeIn(tween(400, delayMillis = 350)) + visible = isDropdownExpanded && accounts.size > 1, + enter = fadeIn(tween(200)) + expandVertically(expandFrom = Alignment.Top), + exit = fadeOut(tween(150)) + shrinkVertically(shrinkTowards = Alignment.Top) ) { - 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) + Card( + 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) + ) { + LazyColumn( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp) ) { - Row( - modifier = Modifier.fillMaxWidth().padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - // Avatar - if (selectedAccount != null) { + items(accounts, key = { it.publicKey }) { account -> + val isSelected = account.publicKey == selectedAccount?.publicKey + + Row( + modifier = Modifier + .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 avatarRepository = remember(selectedAccount!!.publicKey) { - AvatarRepository(context, database.avatarDao(), selectedAccount!!.publicKey) + val avatarRepository = remember(account.publicKey) { + AvatarRepository(context, database.avatarDao(), account.publicKey) } AvatarImage( - publicKey = selectedAccount!!.publicKey, + publicKey = account.publicKey, avatarRepository = avatarRepository, - size = 48.dp, + size = 40.dp, isDarkTheme = isDarkTheme ) - } - Spacer(modifier = Modifier.width(12.dp)) + 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) { - val displayText = selectedAccount!!.encryptedAccount.username - ?: selectedAccount!!.publicKey.take(20) + "..." + 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 = 13.sp, + fontSize = 12.sp, color = secondaryTextColor, maxLines = 1, overflow = TextOverflow.Ellipsis ) } - } - // Dropdown arrow with rotation (only show if multiple accounts) - if (accounts.size > 1) { - Icon( - imageVector = TablerIcons.ChevronDown, - contentDescription = null, - tint = secondaryTextColor.copy(alpha = 0.6f), - 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) + if (isSelected) { + Icon( + TablerIcons.Check, + contentDescription = null, + tint = PrimaryBlue, + modifier = Modifier.size(20.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( 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,30 +558,29 @@ 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( - focusedBorderColor = PrimaryBlue, - unfocusedBorderColor = - if (isDarkTheme) Color(0xFF4A4A4A) - else Color(0xFFD0D0D0), - focusedLabelColor = PrimaryBlue, - cursorColor = PrimaryBlue, - focusedTextColor = textColor, - unfocusedTextColor = textColor, - errorBorderColor = Color(0xFFE53935) - ), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = PrimaryBlue, + unfocusedBorderColor = Color.Transparent, + focusedContainerColor = cardBackground, + unfocusedContainerColor = cardBackground, + focusedTextColor = textColor, + unfocusedTextColor = textColor, + cursorColor = PrimaryBlue, + errorBorderColor = Color(0xFFE53935), + errorContainerColor = cardBackground + ), modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp), - keyboardOptions = - KeyboardOptions( - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Done - ) + shape = RoundedCornerShape(14.dp), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ) ) } @@ -722,136 +596,159 @@ fun UnlockScreen( 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( visible = visible && !isDropdownExpanded, enter = fadeIn(tween(400, delayMillis = 500)) ) { - Column { - // Check if biometric unlock is available for selected account - 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, + modifier = Modifier.padding(bottom = 32.dp) + ) { + // Π Π°Π·Π΄Π΅Π»ΠΈΡ‚Π΅Π»ΡŒ Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) + verticalAlignment = Alignment.CenterVertically ) { - // Password Unlock Button - 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 - .weight(1f) - .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) - ) { - 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) - ) - } - } + 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(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Create or import account", + color = PrimaryBlue, + fontSize = 15.sp, + fontWeight = FontWeight.Medium + ) } } } - - 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)) } } }