Enhance OnboardingScreen with smoother pager swipes and add pulse animation to Rosetta logo
- Implemented custom fling behavior for HorizontalPager to improve swipe experience. - Added pulse animation effect to the Rosetta logo on the onboarding screen for better visual appeal. - Updated the layout and animation specifications to enhance user interaction and aesthetics.
This commit is contained in:
@@ -6,6 +6,7 @@ import androidx.compose.runtime.*
|
||||
import com.rosetta.messenger.data.DecryptedAccount
|
||||
|
||||
enum class AuthScreen {
|
||||
SELECT_ACCOUNT,
|
||||
WELCOME,
|
||||
SEED_PHRASE,
|
||||
CONFIRM_SEED,
|
||||
@@ -18,12 +19,22 @@ enum class AuthScreen {
|
||||
fun AuthFlow(
|
||||
isDarkTheme: Boolean,
|
||||
hasExistingAccount: Boolean,
|
||||
onAuthComplete: (DecryptedAccount?) -> Unit
|
||||
accounts: List<AccountInfo> = emptyList(),
|
||||
onAuthComplete: (DecryptedAccount?) -> Unit,
|
||||
onLogout: () -> Unit = {}
|
||||
) {
|
||||
var currentScreen by remember {
|
||||
mutableStateOf(if (hasExistingAccount) AuthScreen.UNLOCK else AuthScreen.WELCOME)
|
||||
mutableStateOf(
|
||||
when {
|
||||
hasExistingAccount && accounts.size > 1 -> AuthScreen.SELECT_ACCOUNT
|
||||
hasExistingAccount -> AuthScreen.UNLOCK
|
||||
else -> AuthScreen.WELCOME
|
||||
}
|
||||
)
|
||||
}
|
||||
var seedPhrase by remember { mutableStateOf<List<String>>(emptyList()) }
|
||||
var selectedAccountId by remember { mutableStateOf<String?>(accounts.firstOrNull()?.id) }
|
||||
var showCreateModal by remember { mutableStateOf(false) }
|
||||
|
||||
AnimatedContent(
|
||||
targetState = currentScreen,
|
||||
@@ -34,6 +45,29 @@ fun AuthFlow(
|
||||
label = "authScreenTransition"
|
||||
) { screen ->
|
||||
when (screen) {
|
||||
AuthScreen.SELECT_ACCOUNT -> {
|
||||
SelectAccountScreen(
|
||||
isDarkTheme = isDarkTheme,
|
||||
accounts = accounts,
|
||||
selectedAccountId = selectedAccountId,
|
||||
onSelectAccount = { accountId ->
|
||||
selectedAccountId = accountId
|
||||
currentScreen = AuthScreen.UNLOCK
|
||||
},
|
||||
onAddAccount = { showCreateModal = true },
|
||||
showCreateModal = showCreateModal,
|
||||
onCreateNew = {
|
||||
showCreateModal = false
|
||||
currentScreen = AuthScreen.SEED_PHRASE
|
||||
},
|
||||
onImportSeed = {
|
||||
showCreateModal = false
|
||||
currentScreen = AuthScreen.IMPORT_SEED
|
||||
},
|
||||
onDismissModal = { showCreateModal = false }
|
||||
)
|
||||
}
|
||||
|
||||
AuthScreen.WELCOME -> {
|
||||
WelcomeScreen(
|
||||
isDarkTheme = isDarkTheme,
|
||||
@@ -45,7 +79,11 @@ fun AuthFlow(
|
||||
AuthScreen.SEED_PHRASE -> {
|
||||
SeedPhraseScreen(
|
||||
isDarkTheme = isDarkTheme,
|
||||
onBack = { currentScreen = AuthScreen.WELCOME },
|
||||
onBack = {
|
||||
currentScreen = if (hasExistingAccount && accounts.size > 1)
|
||||
AuthScreen.SELECT_ACCOUNT
|
||||
else AuthScreen.WELCOME
|
||||
},
|
||||
onConfirm = { words ->
|
||||
seedPhrase = words
|
||||
currentScreen = AuthScreen.CONFIRM_SEED
|
||||
@@ -74,7 +112,11 @@ fun AuthFlow(
|
||||
AuthScreen.IMPORT_SEED -> {
|
||||
ImportSeedPhraseScreen(
|
||||
isDarkTheme = isDarkTheme,
|
||||
onBack = { currentScreen = AuthScreen.WELCOME },
|
||||
onBack = {
|
||||
currentScreen = if (hasExistingAccount && accounts.size > 1)
|
||||
AuthScreen.SELECT_ACCOUNT
|
||||
else AuthScreen.WELCOME
|
||||
},
|
||||
onSeedPhraseImported = { words ->
|
||||
seedPhrase = words
|
||||
currentScreen = AuthScreen.SET_PASSWORD
|
||||
@@ -85,7 +127,13 @@ fun AuthFlow(
|
||||
AuthScreen.UNLOCK -> {
|
||||
UnlockScreen(
|
||||
isDarkTheme = isDarkTheme,
|
||||
onUnlocked = { account -> onAuthComplete(account) }
|
||||
selectedAccountId = selectedAccountId,
|
||||
onUnlocked = { account -> onAuthComplete(account) },
|
||||
onSwitchAccount = {
|
||||
if (accounts.size > 1) {
|
||||
currentScreen = AuthScreen.SELECT_ACCOUNT
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,8 @@ import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
@@ -17,19 +13,33 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.rosetta.messenger.crypto.CryptoManager
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
|
||||
// Beautiful solid colors that fit the theme
|
||||
private val wordColors = listOf(
|
||||
Color(0xFF5E9FFF), // Soft blue
|
||||
Color(0xFFFF7EB3), // Soft pink
|
||||
Color(0xFF7B68EE), // Medium purple
|
||||
Color(0xFF50C878), // Emerald green
|
||||
Color(0xFFFF6B6B), // Coral red
|
||||
Color(0xFF4ECDC4), // Teal
|
||||
Color(0xFFFFB347), // Pastel orange
|
||||
Color(0xFFBA55D3), // Medium orchid
|
||||
Color(0xFF87CEEB), // Sky blue
|
||||
Color(0xFFDDA0DD), // Plum
|
||||
Color(0xFF98D8C8), // Mint
|
||||
Color(0xFFF7DC6F) // Soft yellow
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ConfirmSeedPhraseScreen(
|
||||
seedPhrase: List<String>,
|
||||
@@ -37,18 +47,16 @@ fun ConfirmSeedPhraseScreen(
|
||||
onBack: () -> Unit,
|
||||
onConfirmed: () -> Unit
|
||||
) {
|
||||
val themeAnimSpec = tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f))
|
||||
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 cardColor by animateColorAsState(if (isDarkTheme) AuthSurface else AuthSurfaceLight, animationSpec = themeAnimSpec)
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF1E1E1E) else Color(0xFFFFFFFF)
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
|
||||
// Select 4 random words to confirm
|
||||
// Select 4 words at fixed positions to confirm (2, 5, 9, 12)
|
||||
val wordsToConfirm = remember {
|
||||
listOf(1, 4, 8, 11).map { index -> index to seedPhrase[index] }
|
||||
}
|
||||
|
||||
var userInputs by remember { mutableStateOf(List(4) { "" }) }
|
||||
var userInputs by remember { mutableStateOf(List(12) { "" }) }
|
||||
var showError by remember { mutableStateOf(false) }
|
||||
var visible by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -56,43 +64,33 @@ fun ConfirmSeedPhraseScreen(
|
||||
visible = true
|
||||
}
|
||||
|
||||
val allCorrect = wordsToConfirm.mapIndexed { i, (_, word) ->
|
||||
userInputs[i].trim().lowercase() == word.lowercase()
|
||||
}.all { it }
|
||||
// Check if the 4 confirmation words are correct
|
||||
val allCorrect = wordsToConfirm.all { (index, word) ->
|
||||
userInputs[index].trim().lowercase() == word.lowercase()
|
||||
}
|
||||
|
||||
// Check if all 4 words have input
|
||||
val allFilled = wordsToConfirm.all { (index, _) ->
|
||||
userInputs[index].isNotBlank()
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(backgroundColor)
|
||||
.statusBarsPadding()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.statusBarsPadding()
|
||||
) {
|
||||
// Top Bar
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Top bar
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
.padding(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = textColor
|
||||
)
|
||||
Icon(Icons.Default.ArrowBack, "Back", tint = textColor)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = "Confirm Backup",
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = textColor
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.width(48.dp))
|
||||
}
|
||||
|
||||
Column(
|
||||
@@ -104,82 +102,127 @@ fun ConfirmSeedPhraseScreen(
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Info Card
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(500)) + slideInVertically(
|
||||
initialOffsetY = { -30 },
|
||||
initialOffsetY = { -20 },
|
||||
animationSpec = tween(500)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(cardColor)
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = PrimaryBlue,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = "Enter the following words from your seed phrase to confirm you've backed it up correctly.",
|
||||
fontSize = 14.sp,
|
||||
color = secondaryTextColor,
|
||||
lineHeight = 18.sp
|
||||
text = "Confirm Backup",
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = textColor
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(500, delayMillis = 100)) + slideInVertically(
|
||||
initialOffsetY = { -20 },
|
||||
animationSpec = tween(500, delayMillis = 100)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Enter words #${wordsToConfirm[0].first + 1}, #${wordsToConfirm[1].first + 1}, #${wordsToConfirm[2].first + 1}, #${wordsToConfirm[3].first + 1}\nto confirm you've backed up your phrase.",
|
||||
fontSize = 15.sp,
|
||||
color = secondaryTextColor,
|
||||
textAlign = TextAlign.Center,
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Word inputs
|
||||
wordsToConfirm.forEachIndexed { index, (wordIndex, _) ->
|
||||
val isCorrect = userInputs[index].trim().lowercase() ==
|
||||
wordsToConfirm[index].second.lowercase()
|
||||
val hasInput = userInputs[index].isNotBlank()
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(500, delayMillis = 100 + (index * 100))) + slideInVertically(
|
||||
initialOffsetY = { 50 },
|
||||
animationSpec = tween(500, delayMillis = 100 + (index * 100))
|
||||
)
|
||||
// Two column layout like SeedPhraseScreen
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(500, delayMillis = 200))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
WordInputField(
|
||||
wordNumber = wordIndex + 1,
|
||||
value = userInputs[index],
|
||||
onValueChange = {
|
||||
userInputs = userInputs.toMutableList().apply {
|
||||
this[index] = it
|
||||
}
|
||||
showError = false
|
||||
},
|
||||
isCorrect = if (hasInput) isCorrect else null,
|
||||
isDarkTheme = isDarkTheme
|
||||
)
|
||||
// Left column (words 1-6)
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
for (i in 0..5) {
|
||||
val needsInput = wordsToConfirm.any { it.first == i }
|
||||
val correctWord = seedPhrase[i]
|
||||
|
||||
AnimatedConfirmWordItem(
|
||||
number = i + 1,
|
||||
displayWord = if (needsInput) "" else correctWord,
|
||||
inputValue = if (needsInput) userInputs[i] else "",
|
||||
isInput = needsInput,
|
||||
onValueChange = { newValue ->
|
||||
userInputs = userInputs.toMutableList().apply {
|
||||
this[i] = newValue.lowercase().trim()
|
||||
}
|
||||
showError = false
|
||||
},
|
||||
isCorrect = if (needsInput && userInputs[i].isNotBlank()) {
|
||||
userInputs[i].trim().lowercase() == correctWord.lowercase()
|
||||
} else null,
|
||||
isDarkTheme = isDarkTheme,
|
||||
visible = visible,
|
||||
delay = 250 + (i * 50)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Right column (words 7-12)
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
for (i in 6..11) {
|
||||
val needsInput = wordsToConfirm.any { it.first == i }
|
||||
val correctWord = seedPhrase[i]
|
||||
|
||||
AnimatedConfirmWordItem(
|
||||
number = i + 1,
|
||||
displayWord = if (needsInput) "" else correctWord,
|
||||
inputValue = if (needsInput) userInputs[i] else "",
|
||||
isInput = needsInput,
|
||||
onValueChange = { newValue ->
|
||||
userInputs = userInputs.toMutableList().apply {
|
||||
this[i] = newValue.lowercase().trim()
|
||||
}
|
||||
showError = false
|
||||
},
|
||||
isCorrect = if (needsInput && userInputs[i].isNotBlank()) {
|
||||
userInputs[i].trim().lowercase() == correctWord.lowercase()
|
||||
} else null,
|
||||
isDarkTheme = isDarkTheme,
|
||||
visible = visible,
|
||||
delay = 250 + (i * 50)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
// Error message
|
||||
AnimatedVisibility(
|
||||
visible = showError,
|
||||
enter = fadeIn() + slideInVertically { -10 },
|
||||
exit = fadeOut()
|
||||
enter = fadeIn(tween(300)) + scaleIn(initialScale = 0.9f),
|
||||
exit = fadeOut(tween(200))
|
||||
) {
|
||||
Text(
|
||||
text = "Some words don't match. Please check and try again.",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFFE53935),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Some words don't match. Please check and try again.",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFFE53935),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
@@ -187,90 +230,208 @@ fun ConfirmSeedPhraseScreen(
|
||||
// Continue Button
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(500, delayMillis = 500)) + slideInVertically(
|
||||
enter = fadeIn(tween(500, delayMillis = 600)) + slideInVertically(
|
||||
initialOffsetY = { 50 },
|
||||
animationSpec = tween(500, delayMillis = 500)
|
||||
animationSpec = tween(500, delayMillis = 600)
|
||||
)
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
if (allCorrect) {
|
||||
onConfirmed()
|
||||
} else {
|
||||
showError = true
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (allCorrect) PrimaryBlue else PrimaryBlue.copy(alpha = 0.5f)
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Confirm",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
onClick = {
|
||||
if (allCorrect) {
|
||||
onConfirmed()
|
||||
} else {
|
||||
showError = true
|
||||
}
|
||||
},
|
||||
enabled = allFilled,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PrimaryBlue,
|
||||
contentColor = Color.White,
|
||||
disabledContainerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8),
|
||||
disabledContentColor = if (isDarkTheme) Color(0xFF666666) else Color(0xFF999999)
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Confirm",
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun WordInputField(
|
||||
wordNumber: Int,
|
||||
private fun AnimatedConfirmWordItem(
|
||||
number: Int,
|
||||
displayWord: String,
|
||||
inputValue: String,
|
||||
isInput: Boolean,
|
||||
onValueChange: (String) -> Unit,
|
||||
isCorrect: Boolean?,
|
||||
isDarkTheme: Boolean,
|
||||
visible: Boolean,
|
||||
delay: Int,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(400, delayMillis = delay)) + slideInHorizontally(
|
||||
initialOffsetX = { if (number <= 6) -50 else 50 },
|
||||
animationSpec = tween(400, delayMillis = delay)
|
||||
)
|
||||
) {
|
||||
if (isInput) {
|
||||
ConfirmWordInputItem(
|
||||
number = number,
|
||||
value = inputValue,
|
||||
onValueChange = onValueChange,
|
||||
isCorrect = isCorrect,
|
||||
isDarkTheme = isDarkTheme,
|
||||
modifier = modifier
|
||||
)
|
||||
} else {
|
||||
DisplayWordItem(
|
||||
number = number,
|
||||
word = displayWord,
|
||||
isDarkTheme = isDarkTheme,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayWordItem(
|
||||
number: Int,
|
||||
word: String,
|
||||
isDarkTheme: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val numberColor = if (isDarkTheme) Color(0xFF888888) else Color(0xFF999999)
|
||||
val bgColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
|
||||
val wordColor = wordColors[(number - 1) % wordColors.size]
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(bgColor)
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "$number.",
|
||||
fontSize = 15.sp,
|
||||
color = numberColor,
|
||||
modifier = Modifier.width(28.dp)
|
||||
)
|
||||
Text(
|
||||
text = word,
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = wordColor,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConfirmWordInputItem(
|
||||
number: Int,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
isCorrect: Boolean?,
|
||||
isDarkTheme: Boolean
|
||||
isDarkTheme: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val labelColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
val numberColor = if (isDarkTheme) Color(0xFF888888) else Color(0xFF999999)
|
||||
val hintColor = if (isDarkTheme) Color(0xFF555555) else Color(0xFFBBBBBB)
|
||||
val bgColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
|
||||
val wordColor = wordColors[(number - 1) % wordColors.size]
|
||||
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
|
||||
// Border color based on correctness
|
||||
val borderColor = when (isCorrect) {
|
||||
true -> Color(0xFF4CAF50)
|
||||
false -> Color(0xFFE53935)
|
||||
null -> if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD0D0D0)
|
||||
true -> Color(0xFF4CAF50) // Green
|
||||
false -> Color(0xFFE53935) // Red
|
||||
null -> if (isFocused) PrimaryBlue else Color.Transparent
|
||||
}
|
||||
|
||||
val trailingIcon: @Composable (() -> Unit)? = when (isCorrect) {
|
||||
true -> {
|
||||
{ Icon(Icons.Default.Check, null, tint = Color(0xFF4CAF50)) }
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.border(
|
||||
width = 2.dp,
|
||||
color = borderColor,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.background(bgColor)
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "$number.",
|
||||
fontSize = 15.sp,
|
||||
color = numberColor,
|
||||
modifier = Modifier.width(28.dp)
|
||||
)
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
textStyle = TextStyle(
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = if (value.isNotEmpty()) wordColor else hintColor,
|
||||
fontFamily = FontFamily.Monospace
|
||||
),
|
||||
singleLine = true,
|
||||
cursorBrush = SolidColor(PrimaryBlue),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.onFocusChanged { isFocused = it.isFocused },
|
||||
decorationBox = { innerTextField ->
|
||||
Box {
|
||||
if (value.isEmpty()) {
|
||||
Text(
|
||||
"enter word",
|
||||
fontSize = 17.sp,
|
||||
color = hintColor,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Show check/cross icon
|
||||
if (isCorrect != null) {
|
||||
Icon(
|
||||
imageVector = if (isCorrect) Icons.Default.Check else Icons.Default.Close,
|
||||
contentDescription = null,
|
||||
tint = if (isCorrect) Color(0xFF4CAF50) else Color(0xFFE53935),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
false -> {
|
||||
{ Icon(Icons.Default.Close, null, tint = Color(0xFFE53935)) }
|
||||
}
|
||||
null -> null
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = { onValueChange(it.lowercase().trim()) },
|
||||
label = { Text("Word #$wordNumber") },
|
||||
placeholder = { Text("Enter word $wordNumber") },
|
||||
singleLine = true,
|
||||
trailingIcon = trailingIcon,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = PrimaryBlue,
|
||||
unfocusedBorderColor = borderColor,
|
||||
focusedLabelColor = PrimaryBlue,
|
||||
unfocusedLabelColor = labelColor,
|
||||
cursorColor = PrimaryBlue,
|
||||
focusedTextColor = textColor,
|
||||
unfocusedTextColor = textColor
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
imeAction = ImeAction.Next
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -35,12 +36,10 @@ fun ImportSeedPhraseScreen(
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF1E1E1E) else Color(0xFFFFFFFF)
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
val cardBackground = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
|
||||
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
var words by remember { mutableStateOf(List(12) { "" }) }
|
||||
var error by remember { mutableStateOf<String?>(null) }
|
||||
var pastedText by remember { mutableStateOf("") }
|
||||
var showPasteDialog by remember { mutableStateOf(false) }
|
||||
var visible by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -49,18 +48,6 @@ fun ImportSeedPhraseScreen(
|
||||
|
||||
val allWordsFilled = words.all { it.isNotBlank() }
|
||||
|
||||
// Parse pasted text
|
||||
LaunchedEffect(pastedText) {
|
||||
if (pastedText.isNotBlank()) {
|
||||
val parsed = pastedText.trim().lowercase().split("\\s+".toRegex())
|
||||
if (parsed.size == 12) {
|
||||
words = parsed
|
||||
showPasteDialog = false
|
||||
error = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -68,7 +55,7 @@ fun ImportSeedPhraseScreen(
|
||||
.statusBarsPadding()
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Simple top bar
|
||||
// Top bar
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -80,17 +67,13 @@ fun ImportSeedPhraseScreen(
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(500, easing = FastOutSlowInEasing))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
@@ -111,98 +94,140 @@ fun ImportSeedPhraseScreen(
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(500, delayMillis = 100))
|
||||
enter = fadeIn(tween(500, delayMillis = 100)) + slideInVertically(
|
||||
initialOffsetY = { -20 },
|
||||
animationSpec = tween(500, delayMillis = 100)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Enter your 12-word recovery phrase",
|
||||
text = "Enter your 12-word recovery phrase\nto restore your account.",
|
||||
fontSize = 15.sp,
|
||||
color = secondaryTextColor,
|
||||
textAlign = TextAlign.Center
|
||||
textAlign = TextAlign.Center,
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Paste button
|
||||
// Paste button - directly reads from clipboard
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(500, delayMillis = 200)) + scaleIn(
|
||||
enter = fadeIn(tween(500, delayMillis = 150)) + scaleIn(
|
||||
initialScale = 0.9f,
|
||||
animationSpec = tween(500, delayMillis = 200)
|
||||
animationSpec = tween(500, delayMillis = 150)
|
||||
)
|
||||
) {
|
||||
Button(
|
||||
onClick = { showPasteDialog = true },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PrimaryBlue,
|
||||
contentColor = Color.White
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ContentPaste,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(
|
||||
"Paste All 12 Words",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
val clipboardText = clipboardManager.getText()?.text
|
||||
if (clipboardText != null && clipboardText.isNotBlank()) {
|
||||
val parsed = clipboardText.trim().lowercase()
|
||||
.split(Regex("[\\s,;]+"))
|
||||
.filter { it.isNotBlank() }
|
||||
.map { it.trim() }
|
||||
|
||||
when {
|
||||
parsed.size == 12 -> {
|
||||
words = parsed.toList()
|
||||
error = null
|
||||
}
|
||||
parsed.size > 12 -> {
|
||||
words = parsed.take(12).toList()
|
||||
error = null
|
||||
}
|
||||
parsed.isNotEmpty() -> {
|
||||
error = "Clipboard contains ${parsed.size} words, need 12"
|
||||
}
|
||||
else -> {
|
||||
error = "No valid words found in clipboard"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error = "Clipboard is empty"
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ContentPaste,
|
||||
contentDescription = null,
|
||||
tint = PrimaryBlue,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
"Paste all 12 words",
|
||||
color = PrimaryBlue,
|
||||
fontSize = 15.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
// Clean grid
|
||||
// Two column layout like SeedPhraseScreen
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(500, delayMillis = 300))
|
||||
enter = fadeIn(tween(500, delayMillis = 200))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(cardBackground)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
for (row in 0..3) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Left column (words 1-6)
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
for (col in 0..2) {
|
||||
val index = row * 3 + col
|
||||
WordInputItem(
|
||||
number = index + 1,
|
||||
value = words[index],
|
||||
for (i in 0..5) {
|
||||
AnimatedWordInputItem(
|
||||
number = i + 1,
|
||||
value = words[i],
|
||||
onValueChange = { newValue ->
|
||||
words = words.toMutableList().apply {
|
||||
this[index] = newValue.lowercase().trim()
|
||||
this[i] = newValue.lowercase().trim()
|
||||
}
|
||||
error = null
|
||||
},
|
||||
isDarkTheme = isDarkTheme,
|
||||
modifier = Modifier.weight(1f)
|
||||
visible = visible,
|
||||
delay = 250 + (i * 50)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Right column (words 7-12)
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
for (i in 6..11) {
|
||||
AnimatedWordInputItem(
|
||||
number = i + 1,
|
||||
value = words[i],
|
||||
onValueChange = { newValue ->
|
||||
words = words.toMutableList().apply {
|
||||
this[i] = newValue.lowercase().trim()
|
||||
}
|
||||
error = null
|
||||
},
|
||||
isDarkTheme = isDarkTheme,
|
||||
visible = visible,
|
||||
delay = 250 + (i * 50)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error
|
||||
if (error != null) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = fadeIn(tween(300)) + scaleIn(initialScale = 0.9f)
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = error != null,
|
||||
enter = fadeIn(tween(300)) + scaleIn(initialScale = 0.9f),
|
||||
exit = fadeOut(tween(200))
|
||||
) {
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = error ?: "",
|
||||
fontSize = 14.sp,
|
||||
@@ -217,95 +242,80 @@ fun ImportSeedPhraseScreen(
|
||||
// Import button
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(500, delayMillis = 400)) + slideInVertically(
|
||||
enter = fadeIn(tween(500, delayMillis = 600)) + slideInVertically(
|
||||
initialOffsetY = { 50 },
|
||||
animationSpec = tween(500, delayMillis = 400)
|
||||
animationSpec = tween(500, delayMillis = 600)
|
||||
)
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
val seedPhrase = words.map { it.trim() }
|
||||
|
||||
if (seedPhrase.any { it.isBlank() }) {
|
||||
error = "Please fill in all words"
|
||||
return@Button
|
||||
}
|
||||
|
||||
if (!CryptoManager.validateSeedPhrase(seedPhrase)) {
|
||||
error = "Invalid recovery phrase"
|
||||
return@Button
|
||||
}
|
||||
|
||||
onSeedPhraseImported(seedPhrase)
|
||||
},
|
||||
enabled = allWordsFilled,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PrimaryBlue,
|
||||
contentColor = Color.White,
|
||||
disabledContainerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8),
|
||||
disabledContentColor = if (isDarkTheme) Color(0xFF666666) else Color(0xFF999999)
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Text("Continue", fontSize = 17.sp, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Paste dialog
|
||||
if (showPasteDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showPasteDialog = false },
|
||||
title = {
|
||||
Text("Paste Recovery Phrase", fontWeight = FontWeight.Bold)
|
||||
},
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = pastedText,
|
||||
onValueChange = { pastedText = it },
|
||||
placeholder = { Text("Paste your 12 words here") },
|
||||
onClick = {
|
||||
val seedPhrase = words.map { it.trim() }
|
||||
|
||||
if (seedPhrase.any { it.isBlank() }) {
|
||||
error = "Please fill in all words"
|
||||
return@Button
|
||||
}
|
||||
|
||||
if (!CryptoManager.validateSeedPhrase(seedPhrase)) {
|
||||
error = "Invalid recovery phrase"
|
||||
return@Button
|
||||
}
|
||||
|
||||
onSeedPhraseImported(seedPhrase)
|
||||
},
|
||||
enabled = allWordsFilled,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(120.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = PrimaryBlue,
|
||||
cursorColor = PrimaryBlue
|
||||
.height(50.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PrimaryBlue,
|
||||
contentColor = Color.White,
|
||||
disabledContainerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8),
|
||||
disabledContentColor = if (isDarkTheme) Color(0xFF666666) else Color(0xFF999999)
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val parsed = pastedText.trim().lowercase().split("\\s+".toRegex())
|
||||
if (parsed.size == 12) {
|
||||
words = parsed
|
||||
showPasteDialog = false
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text("Import", color = PrimaryBlue, fontWeight = FontWeight.Medium)
|
||||
Text(
|
||||
text = "Continue",
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showPasteDialog = false }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
},
|
||||
containerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color.White,
|
||||
shape = RoundedCornerShape(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimatedWordInputItem(
|
||||
number: Int,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
isDarkTheme: Boolean,
|
||||
visible: Boolean,
|
||||
delay: Int,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(400, delayMillis = delay)) + slideInHorizontally(
|
||||
initialOffsetX = { if (number <= 6) -50 else 50 },
|
||||
animationSpec = tween(400, delayMillis = delay)
|
||||
)
|
||||
) {
|
||||
WordInputItem(
|
||||
number = number,
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
isDarkTheme = isDarkTheme,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WordInputItem(
|
||||
number: Int,
|
||||
@@ -314,55 +324,81 @@ private fun WordInputItem(
|
||||
isDarkTheme: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val itemBg = if (isDarkTheme) Color(0xFF1E1E1E) else Color.White
|
||||
val numberColor = if (isDarkTheme) Color(0xFF666666) else Color(0xFFAAAAAA)
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val hintColor = if (isDarkTheme) Color(0xFF666666) else Color(0xFFAAAAAA)
|
||||
val numberColor = if (isDarkTheme) Color(0xFF888888) else Color(0xFF999999)
|
||||
val hintColor = if (isDarkTheme) Color(0xFF555555) else Color(0xFFBBBBBB)
|
||||
val bgColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
|
||||
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val borderColor = if (isFocused) PrimaryBlue else Color.Transparent
|
||||
|
||||
Row(
|
||||
// Beautiful solid colors that fit the theme - same as SeedPhraseScreen
|
||||
val wordColors = listOf(
|
||||
Color(0xFF5E9FFF), // Soft blue
|
||||
Color(0xFFFF7EB3), // Soft pink
|
||||
Color(0xFF7B68EE), // Medium purple
|
||||
Color(0xFF50C878), // Emerald green
|
||||
Color(0xFFFF6B6B), // Coral red
|
||||
Color(0xFF4ECDC4), // Teal
|
||||
Color(0xFFFFB347), // Pastel orange
|
||||
Color(0xFFBA55D3), // Medium orchid
|
||||
Color(0xFF87CEEB), // Sky blue
|
||||
Color(0xFFDDA0DD), // Plum
|
||||
Color(0xFF98D8C8), // Mint
|
||||
Color(0xFFF7DC6F) // Soft yellow
|
||||
)
|
||||
|
||||
val wordColor = wordColors[(number - 1) % wordColors.size]
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.border(1.dp, borderColor, RoundedCornerShape(8.dp))
|
||||
.background(itemBg)
|
||||
.padding(horizontal = 10.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.border(
|
||||
width = if (isFocused) 2.dp else 0.dp,
|
||||
color = if (isFocused) PrimaryBlue else Color.Transparent,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.background(bgColor)
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "$number.",
|
||||
fontSize = 13.sp,
|
||||
color = numberColor,
|
||||
modifier = Modifier.width(22.dp)
|
||||
)
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
textStyle = TextStyle(
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "$number.",
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = textColor,
|
||||
fontFamily = FontFamily.Monospace
|
||||
),
|
||||
singleLine = true,
|
||||
cursorBrush = SolidColor(PrimaryBlue),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.onFocusChanged { isFocused = it.isFocused },
|
||||
decorationBox = { innerTextField ->
|
||||
Box {
|
||||
if (value.isEmpty()) {
|
||||
Text(
|
||||
"word",
|
||||
fontSize = 15.sp,
|
||||
color = hintColor.copy(alpha = 0.5f),
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
color = numberColor,
|
||||
modifier = Modifier.width(28.dp)
|
||||
)
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
textStyle = TextStyle(
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = if (value.isNotEmpty()) wordColor else hintColor,
|
||||
fontFamily = FontFamily.Monospace
|
||||
),
|
||||
singleLine = true,
|
||||
cursorBrush = SolidColor(PrimaryBlue),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.onFocusChanged { isFocused = it.isFocused },
|
||||
decorationBox = { innerTextField ->
|
||||
Box {
|
||||
if (value.isEmpty()) {
|
||||
Text(
|
||||
"word",
|
||||
fontSize = 17.sp,
|
||||
color = hintColor,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -248,31 +249,52 @@ private fun WordItem(
|
||||
isDarkTheme: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val itemBg = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
|
||||
val numberColor = if (isDarkTheme) Color(0xFF666666) else Color(0xFFAAAAAA)
|
||||
val wordColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val numberColor = if (isDarkTheme) Color(0xFF888888) else Color(0xFF999999)
|
||||
|
||||
Row(
|
||||
// Beautiful solid colors that fit the theme
|
||||
val wordColors = listOf(
|
||||
Color(0xFF5E9FFF), // Soft blue
|
||||
Color(0xFFFF7EB3), // Soft pink
|
||||
Color(0xFF7B68EE), // Medium purple
|
||||
Color(0xFF50C878), // Emerald green
|
||||
Color(0xFFFF6B6B), // Coral red
|
||||
Color(0xFF4ECDC4), // Teal
|
||||
Color(0xFFFFB347), // Pastel orange
|
||||
Color(0xFFBA55D3), // Medium orchid
|
||||
Color(0xFF87CEEB), // Sky blue
|
||||
Color(0xFFDDA0DD), // Plum
|
||||
Color(0xFF98D8C8), // Mint
|
||||
Color(0xFFF7DC6F) // Soft yellow
|
||||
)
|
||||
|
||||
val wordColor = wordColors[(number - 1) % wordColors.size]
|
||||
val bgColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(itemBg)
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.background(bgColor)
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "$number.",
|
||||
fontSize = 15.sp,
|
||||
color = numberColor,
|
||||
modifier = Modifier.width(28.dp)
|
||||
)
|
||||
Text(
|
||||
text = word,
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = wordColor,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "$number.",
|
||||
fontSize = 15.sp,
|
||||
color = numberColor,
|
||||
modifier = Modifier.width(28.dp)
|
||||
)
|
||||
Text(
|
||||
text = word,
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = wordColor,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
package com.rosetta.messenger.ui.auth
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
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.ui.onboarding.PrimaryBlue
|
||||
|
||||
data class AccountInfo(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val initials: String,
|
||||
val publicKey: String
|
||||
)
|
||||
|
||||
// Avatar colors for accounts
|
||||
private val accountColors = listOf(
|
||||
Color(0xFF5E9FFF), // Blue
|
||||
Color(0xFFFF7EB3), // Pink
|
||||
Color(0xFF7B68EE), // Purple
|
||||
Color(0xFF50C878), // Green
|
||||
Color(0xFFFF6B6B), // Red
|
||||
Color(0xFF4ECDC4), // Teal
|
||||
Color(0xFFFFB347), // Orange
|
||||
Color(0xFFBA55D3) // Orchid
|
||||
)
|
||||
|
||||
fun getAccountColor(name: String): Color {
|
||||
val index = name.hashCode().mod(accountColors.size).let { if (it < 0) it + accountColors.size else it }
|
||||
return accountColors[index]
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SelectAccountScreen(
|
||||
isDarkTheme: Boolean,
|
||||
accounts: List<AccountInfo>,
|
||||
selectedAccountId: String?,
|
||||
onSelectAccount: (String) -> Unit,
|
||||
onAddAccount: () -> Unit,
|
||||
showCreateModal: Boolean,
|
||||
onCreateNew: () -> Unit,
|
||||
onImportSeed: () -> Unit,
|
||||
onDismissModal: () -> Unit
|
||||
) {
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF1E1E1E) else Color(0xFFFFFFFF)
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
|
||||
var visible by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
visible = true
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(backgroundColor)
|
||||
.statusBarsPadding()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 32.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(60.dp))
|
||||
|
||||
// Header
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(500)) + slideInVertically(
|
||||
initialOffsetY = { -30 },
|
||||
animationSpec = tween(500)
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
Row {
|
||||
Text(
|
||||
text = "Select ",
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = PrimaryBlue
|
||||
)
|
||||
Text(
|
||||
text = "account",
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = textColor
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Select your account for login,\nor add new account",
|
||||
fontSize = 15.sp,
|
||||
color = secondaryTextColor,
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
|
||||
// Accounts grid
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(500, delayMillis = 200))
|
||||
) {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
items(accounts, key = { it.id }) { account ->
|
||||
val index = accounts.indexOf(account)
|
||||
AccountCard(
|
||||
account = account,
|
||||
isSelected = account.id == selectedAccountId,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onClick = { onSelectAccount(account.id) },
|
||||
animationDelay = 250 + (index * 100)
|
||||
)
|
||||
}
|
||||
|
||||
// Add Account card
|
||||
item {
|
||||
AddAccountCard(
|
||||
isDarkTheme = isDarkTheme,
|
||||
onClick = onAddAccount,
|
||||
animationDelay = 250 + (accounts.size * 100)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create Account Modal
|
||||
if (showCreateModal) {
|
||||
CreateAccountModal(
|
||||
isDarkTheme = isDarkTheme,
|
||||
onCreateNew = onCreateNew,
|
||||
onImportSeed = onImportSeed,
|
||||
onDismiss = onDismissModal
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AccountCard(
|
||||
account: AccountInfo,
|
||||
isSelected: Boolean,
|
||||
isDarkTheme: Boolean,
|
||||
onClick: () -> Unit,
|
||||
animationDelay: Int
|
||||
) {
|
||||
val surfaceColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
|
||||
val borderColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0)
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
|
||||
val avatarColor = getAccountColor(account.name)
|
||||
|
||||
var visible by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
kotlinx.coroutines.delay(animationDelay.toLong())
|
||||
visible = true
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(400)) + scaleIn(
|
||||
initialScale = 0.9f,
|
||||
animationSpec = tween(400)
|
||||
)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(0.85f)
|
||||
.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isSelected) PrimaryBlue.copy(alpha = 0.1f) else surfaceColor
|
||||
),
|
||||
border = BorderStroke(
|
||||
width = 2.dp,
|
||||
color = if (isSelected) PrimaryBlue else borderColor
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// Checkmark
|
||||
if (isSelected) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(12.dp)
|
||||
.size(24.dp)
|
||||
.clip(CircleShape)
|
||||
.background(PrimaryBlue),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(12.dp)
|
||||
.size(24.dp)
|
||||
.clip(CircleShape)
|
||||
.border(2.dp, borderColor, CircleShape)
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(20.dp)
|
||||
) {
|
||||
// Avatar
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.clip(CircleShape)
|
||||
.background(avatarColor.copy(alpha = 0.2f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = account.initials,
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = avatarColor
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Name
|
||||
Text(
|
||||
text = account.name,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (isSelected) PrimaryBlue else textColor,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddAccountCard(
|
||||
isDarkTheme: Boolean,
|
||||
onClick: () -> Unit,
|
||||
animationDelay: Int
|
||||
) {
|
||||
val surfaceColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
|
||||
val borderColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0)
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
|
||||
var visible by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
kotlinx.coroutines.delay(animationDelay.toLong())
|
||||
visible = true
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(400)) + scaleIn(
|
||||
initialScale = 0.9f,
|
||||
animationSpec = tween(400)
|
||||
)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(0.85f)
|
||||
.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = surfaceColor),
|
||||
border = BorderStroke(
|
||||
width = 2.dp,
|
||||
color = borderColor,
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(20.dp)
|
||||
) {
|
||||
// Plus icon
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.clip(CircleShape)
|
||||
.border(2.dp, PrimaryBlue, CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
tint = PrimaryBlue,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "Add Account",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = PrimaryBlue,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CreateAccountModal(
|
||||
isDarkTheme: Boolean,
|
||||
onCreateNew: () -> Unit,
|
||||
onImportSeed: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFFFFFFF)
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.5f))
|
||||
.clickable(onClick = onDismiss),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.padding(32.dp)
|
||||
.clickable(enabled = false, onClick = {}),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = backgroundColor)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "Create account",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = textColor
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "You may be create new account\nor import existing",
|
||||
fontSize = 15.sp,
|
||||
color = secondaryTextColor,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Create New button
|
||||
Button(
|
||||
onClick = onCreateNew,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PrimaryBlue,
|
||||
contentColor = Color.White
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Create new",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Import (Recover) button
|
||||
OutlinedButton(
|
||||
onClick = onImportSeed,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = PrimaryBlue
|
||||
),
|
||||
border = BorderStroke(1.dp, PrimaryBlue),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Recover",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,9 @@ import kotlinx.coroutines.launch
|
||||
@Composable
|
||||
fun UnlockScreen(
|
||||
isDarkTheme: Boolean,
|
||||
onUnlocked: (DecryptedAccount) -> Unit
|
||||
selectedAccountId: String? = null,
|
||||
onUnlocked: (DecryptedAccount) -> Unit,
|
||||
onSwitchAccount: () -> Unit = {}
|
||||
) {
|
||||
val themeAnimSpec = tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f))
|
||||
val backgroundColor by animateColorAsState(if (isDarkTheme) AuthBackground else AuthBackgroundLight, animationSpec = themeAnimSpec)
|
||||
@@ -56,7 +58,7 @@ fun UnlockScreen(
|
||||
|
||||
// Load current account
|
||||
LaunchedEffect(Unit) {
|
||||
currentPublicKey = accountManager.currentPublicKey.first()
|
||||
currentPublicKey = selectedAccountId ?: accountManager.currentPublicKey.first()
|
||||
}
|
||||
|
||||
// Entry animation
|
||||
@@ -290,6 +292,34 @@ fun UnlockScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Switch Account button
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(600, delayMillis = 600)) + slideInVertically(
|
||||
initialOffsetY = { 50 },
|
||||
animationSpec = tween(600, delayMillis = 600)
|
||||
)
|
||||
) {
|
||||
TextButton(
|
||||
onClick = onSwitchAccount
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SwapHoriz,
|
||||
contentDescription = null,
|
||||
tint = PrimaryBlue,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Switch Account",
|
||||
color = PrimaryBlue,
|
||||
fontSize = 15.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(0.3f))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,8 @@ fun WelcomeScreen(
|
||||
val lockComposition by rememberLottieComposition(LottieCompositionSpec.Asset("lottie/lock.json"))
|
||||
val lockProgress by animateLottieCompositionAsState(
|
||||
composition = lockComposition,
|
||||
iterations = LottieConstants.IterateForever
|
||||
iterations = 1, // Play once
|
||||
speed = 1f
|
||||
)
|
||||
|
||||
// Entry animation
|
||||
|
||||
Reference in New Issue
Block a user