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:
@@ -23,12 +23,16 @@ import androidx.compose.ui.unit.sp
|
||||
import com.rosetta.messenger.data.AccountManager
|
||||
import com.rosetta.messenger.data.DecryptedAccount
|
||||
import com.rosetta.messenger.data.PreferencesManager
|
||||
import com.rosetta.messenger.ui.auth.AccountInfo
|
||||
import com.rosetta.messenger.ui.auth.AuthFlow
|
||||
import com.rosetta.messenger.ui.chats.Chat
|
||||
import com.rosetta.messenger.ui.chats.ChatsListScreen
|
||||
import com.rosetta.messenger.ui.onboarding.OnboardingScreen
|
||||
import com.rosetta.messenger.ui.splash.SplashScreen
|
||||
import com.rosetta.messenger.ui.theme.RosettaAndroidTheme
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private lateinit var preferencesManager: PreferencesManager
|
||||
@@ -49,11 +53,31 @@ class MainActivity : ComponentActivity() {
|
||||
var showOnboarding by remember { mutableStateOf(true) }
|
||||
var hasExistingAccount by remember { mutableStateOf<Boolean?>(null) }
|
||||
var currentAccount by remember { mutableStateOf<DecryptedAccount?>(null) }
|
||||
var accountInfoList by remember { mutableStateOf<List<AccountInfo>>(emptyList()) }
|
||||
|
||||
// Check for existing accounts
|
||||
// Check for existing accounts and build AccountInfo list
|
||||
LaunchedEffect(Unit) {
|
||||
val accounts = accountManager.getAllAccounts()
|
||||
hasExistingAccount = accounts.isNotEmpty()
|
||||
accountInfoList = accounts.map { account ->
|
||||
val shortKey = account.publicKey.take(7)
|
||||
val displayName = account.name ?: shortKey
|
||||
val initials = displayName.trim().split(Regex("\\s+"))
|
||||
.filter { it.isNotEmpty() }
|
||||
.let { words ->
|
||||
when {
|
||||
words.isEmpty() -> "??"
|
||||
words.size == 1 -> words[0].take(2).uppercase()
|
||||
else -> "${words[0].first()}${words[1].first()}".uppercase()
|
||||
}
|
||||
}
|
||||
AccountInfo(
|
||||
id = account.publicKey,
|
||||
name = displayName,
|
||||
initials = initials,
|
||||
publicKey = account.publicKey
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for initial load
|
||||
@@ -112,15 +136,46 @@ class MainActivity : ComponentActivity() {
|
||||
AuthFlow(
|
||||
isDarkTheme = isDarkTheme,
|
||||
hasExistingAccount = screen == "auth_unlock",
|
||||
accounts = accountInfoList,
|
||||
onAuthComplete = { account ->
|
||||
currentAccount = account
|
||||
hasExistingAccount = true
|
||||
// Reload accounts list
|
||||
scope.launch {
|
||||
val accounts = accountManager.getAllAccounts()
|
||||
accountInfoList = accounts.map { acc ->
|
||||
val shortKey = acc.publicKey.take(7)
|
||||
val displayName = acc.name ?: shortKey
|
||||
val initials = displayName.trim().split(Regex("\\s+"))
|
||||
.filter { it.isNotEmpty() }
|
||||
.let { words ->
|
||||
when {
|
||||
words.isEmpty() -> "??"
|
||||
words.size == 1 -> words[0].take(2).uppercase()
|
||||
else -> "${words[0].first()}${words[1].first()}".uppercase()
|
||||
}
|
||||
}
|
||||
AccountInfo(
|
||||
id = acc.publicKey,
|
||||
name = displayName,
|
||||
initials = initials,
|
||||
publicKey = acc.publicKey
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onLogout = {
|
||||
scope.launch {
|
||||
accountManager.logout()
|
||||
currentAccount = null
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
"main" -> {
|
||||
MainScreen(
|
||||
account = currentAccount,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onLogout = {
|
||||
scope.launch {
|
||||
accountManager.logout()
|
||||
@@ -140,17 +195,54 @@ class MainActivity : ComponentActivity() {
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
account: DecryptedAccount? = null,
|
||||
isDarkTheme: Boolean = true,
|
||||
onLogout: () -> Unit = {}
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Welcome to Rosetta! 🚀\n\nYou're logged in!",
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
// Demo chats for now
|
||||
val demoChats = remember {
|
||||
listOf(
|
||||
Chat(
|
||||
id = "1",
|
||||
name = "Alice Johnson",
|
||||
lastMessage = "Hey! How are you doing?",
|
||||
lastMessageTime = Date(),
|
||||
unreadCount = 2,
|
||||
isOnline = true,
|
||||
publicKey = "alice_key_123"
|
||||
),
|
||||
Chat(
|
||||
id = "2",
|
||||
name = "Bob Smith",
|
||||
lastMessage = "See you tomorrow!",
|
||||
lastMessageTime = Date(System.currentTimeMillis() - 3600000),
|
||||
unreadCount = 0,
|
||||
isOnline = false,
|
||||
publicKey = "bob_key_456"
|
||||
),
|
||||
Chat(
|
||||
id = "3",
|
||||
name = "Team Rosetta",
|
||||
lastMessage = "Great work everyone! 🎉",
|
||||
lastMessageTime = Date(System.currentTimeMillis() - 86400000),
|
||||
unreadCount = 5,
|
||||
isOnline = true,
|
||||
publicKey = "team_key_789"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
ChatsListScreen(
|
||||
isDarkTheme = isDarkTheme,
|
||||
chats = demoChats,
|
||||
onChatClick = { chat ->
|
||||
// TODO: Navigate to chat detail
|
||||
},
|
||||
onNewChat = {
|
||||
// TODO: Show new chat screen
|
||||
},
|
||||
onProfileClick = onLogout, // For now, logout on profile click
|
||||
onSavedMessagesClick = {
|
||||
// TODO: Navigate to saved messages
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
) {
|
||||
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,76 +102,120 @@ 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()
|
||||
|
||||
// Two column layout like SeedPhraseScreen
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(500, delayMillis = 100 + (index * 100))) + slideInVertically(
|
||||
initialOffsetY = { 50 },
|
||||
animationSpec = tween(500, delayMillis = 100 + (index * 100))
|
||||
)
|
||||
enter = fadeIn(tween(500, delayMillis = 200))
|
||||
) {
|
||||
WordInputField(
|
||||
wordNumber = wordIndex + 1,
|
||||
value = userInputs[index],
|
||||
onValueChange = {
|
||||
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 (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[index] = it
|
||||
this[i] = newValue.lowercase().trim()
|
||||
}
|
||||
showError = false
|
||||
},
|
||||
isCorrect = if (hasInput) isCorrect else null,
|
||||
isDarkTheme = isDarkTheme
|
||||
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))
|
||||
// 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error message
|
||||
AnimatedVisibility(
|
||||
visible = showError,
|
||||
enter = fadeIn() + slideInVertically { -10 },
|
||||
exit = fadeOut()
|
||||
enter = fadeIn(tween(300)) + scaleIn(initialScale = 0.9f),
|
||||
exit = fadeOut(tween(200))
|
||||
) {
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Some words don't match. Please check and try again.",
|
||||
fontSize = 14.sp,
|
||||
@@ -181,15 +223,16 @@ fun ConfirmSeedPhraseScreen(
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// 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(
|
||||
@@ -200,77 +243,195 @@ fun ConfirmSeedPhraseScreen(
|
||||
showError = true
|
||||
}
|
||||
},
|
||||
enabled = allFilled,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
.height(50.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (allCorrect) PrimaryBlue else PrimaryBlue.copy(alpha = 0.5f)
|
||||
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 = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
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)) }
|
||||
}
|
||||
false -> {
|
||||
{ Icon(Icons.Default.Close, null, tint = Color(0xFFE53935)) }
|
||||
}
|
||||
null -> null
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
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(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
|
||||
onValueChange = onValueChange,
|
||||
textStyle = TextStyle(
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = if (value.isNotEmpty()) wordColor else hintColor,
|
||||
fontFamily = FontFamily.Monospace
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
imeAction = ImeAction.Next
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,13 +67,9 @@ fun ImportSeedPhraseScreen(
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(500, easing = FastOutSlowInEasing))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
@@ -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)
|
||||
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,
|
||||
modifier = Modifier.size(22.dp)
|
||||
tint = PrimaryBlue,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
"Paste All 12 Words",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
"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)
|
||||
) {
|
||||
for (col in 0..2) {
|
||||
val index = row * 3 + col
|
||||
WordInputItem(
|
||||
number = index + 1,
|
||||
value = words[index],
|
||||
// Left column (words 1-6)
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
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)
|
||||
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,9 +242,9 @@ 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(
|
||||
@@ -250,7 +275,11 @@ fun ImportSeedPhraseScreen(
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Text("Continue", fontSize = 17.sp, fontWeight = FontWeight.Medium)
|
||||
Text(
|
||||
text = "Continue",
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,52 +287,33 @@ fun ImportSeedPhraseScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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") },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(120.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = PrimaryBlue,
|
||||
cursorColor = PrimaryBlue
|
||||
),
|
||||
shape = RoundedCornerShape(12.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)
|
||||
)
|
||||
},
|
||||
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)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showPasteDialog = false }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
},
|
||||
containerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color.White,
|
||||
shape = RoundedCornerShape(20.dp)
|
||||
WordInputItem(
|
||||
number = number,
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
isDarkTheme = isDarkTheme,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -314,35 +324,59 @@ 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)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "$number.",
|
||||
fontSize = 13.sp,
|
||||
fontSize = 15.sp,
|
||||
color = numberColor,
|
||||
modifier = Modifier.width(22.dp)
|
||||
modifier = Modifier.width(28.dp)
|
||||
)
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
textStyle = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = textColor,
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = if (value.isNotEmpty()) wordColor else hintColor,
|
||||
fontFamily = FontFamily.Monospace
|
||||
),
|
||||
singleLine = true,
|
||||
@@ -355,9 +389,10 @@ private fun WordInputItem(
|
||||
if (value.isEmpty()) {
|
||||
Text(
|
||||
"word",
|
||||
fontSize = 15.sp,
|
||||
color = hintColor.copy(alpha = 0.5f),
|
||||
fontFamily = FontFamily.Monospace
|
||||
fontSize = 17.sp,
|
||||
color = hintColor,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
@@ -365,4 +400,5 @@ private fun WordInputItem(
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,17 +249,37 @@ 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)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "$number.",
|
||||
@@ -274,6 +295,7 @@ private fun WordItem(
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
package com.rosetta.messenger.ui.chats
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.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.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
data class Chat(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val lastMessage: String,
|
||||
val lastMessageTime: Date,
|
||||
val unreadCount: Int = 0,
|
||||
val isOnline: Boolean = false,
|
||||
val publicKey: String,
|
||||
val isSavedMessages: Boolean = false
|
||||
)
|
||||
|
||||
// Beautiful avatar colors
|
||||
private val avatarColors = listOf(
|
||||
Color(0xFF5E9FFF) to Color(0xFFE8F1FF), // Blue
|
||||
Color(0xFFFF7EB3) to Color(0xFFFFEEF4), // Pink
|
||||
Color(0xFF7B68EE) to Color(0xFFF0EDFF), // Purple
|
||||
Color(0xFF50C878) to Color(0xFFE8F8EE), // Green
|
||||
Color(0xFFFF6B6B) to Color(0xFFFFEEEE), // Red
|
||||
Color(0xFF4ECDC4) to Color(0xFFE8F8F7), // Teal
|
||||
Color(0xFFFFB347) to Color(0xFFFFF5E8), // Orange
|
||||
Color(0xFFBA55D3) to Color(0xFFF8EEFF) // Orchid
|
||||
)
|
||||
|
||||
fun getAvatarColor(name: String, isDark: Boolean): Pair<Color, Color> {
|
||||
val index = name.hashCode().mod(avatarColors.size).let { if (it < 0) it + avatarColors.size else it }
|
||||
val (primary, light) = avatarColors[index]
|
||||
return if (isDark) primary to primary.copy(alpha = 0.2f) else primary to light
|
||||
}
|
||||
|
||||
fun getInitials(name: String): String {
|
||||
val words = name.trim().split(Regex("\\s+")).filter { it.isNotEmpty() }
|
||||
return when {
|
||||
words.isEmpty() -> "??"
|
||||
words.size == 1 -> words[0].take(2).uppercase()
|
||||
else -> "${words[0].first()}${words[1].first()}".uppercase()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChatsListScreen(
|
||||
isDarkTheme: Boolean,
|
||||
chats: List<Chat>,
|
||||
onChatClick: (Chat) -> Unit,
|
||||
onNewChat: () -> Unit,
|
||||
onProfileClick: () -> Unit,
|
||||
onSavedMessagesClick: () -> 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)
|
||||
val surfaceColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
|
||||
|
||||
var visible by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
visible = true
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(400)) + slideInVertically(
|
||||
initialOffsetY = { -it },
|
||||
animationSpec = tween(400)
|
||||
)
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
"Chats",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 28.sp
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onNewChat) {
|
||||
Icon(
|
||||
Icons.Default.Edit,
|
||||
contentDescription = "New Chat",
|
||||
tint = PrimaryBlue
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onProfileClick) {
|
||||
Icon(
|
||||
Icons.Default.Person,
|
||||
contentDescription = "Profile",
|
||||
tint = textColor
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = backgroundColor,
|
||||
titleContentColor = textColor
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
containerColor = backgroundColor
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentPadding = PaddingValues(vertical = 8.dp)
|
||||
) {
|
||||
// Saved Messages section
|
||||
item {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(400, delayMillis = 100)) + slideInHorizontally(
|
||||
initialOffsetX = { -50 },
|
||||
animationSpec = tween(400, delayMillis = 100)
|
||||
)
|
||||
) {
|
||||
SavedMessagesItem(
|
||||
isDarkTheme = isDarkTheme,
|
||||
onClick = onSavedMessagesClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Chat items
|
||||
items(chats, key = { it.id }) { chat ->
|
||||
val index = chats.indexOf(chat)
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(400, delayMillis = 150 + (index * 50))) + slideInHorizontally(
|
||||
initialOffsetX = { -50 },
|
||||
animationSpec = tween(400, delayMillis = 150 + (index * 50))
|
||||
)
|
||||
) {
|
||||
ChatItem(
|
||||
chat = chat,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onClick = { onChatClick(chat) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state
|
||||
if (chats.isEmpty()) {
|
||||
item {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(400, delayMillis = 200)) + scaleIn(
|
||||
initialScale = 0.9f,
|
||||
animationSpec = tween(400, delayMillis = 200)
|
||||
)
|
||||
) {
|
||||
EmptyChatsState(
|
||||
isDarkTheme = isDarkTheme,
|
||||
onNewChat = onNewChat
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SavedMessagesItem(
|
||||
isDarkTheme: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Saved Messages icon
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(56.dp)
|
||||
.clip(CircleShape)
|
||||
.background(PrimaryBlue),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Bookmark,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Saved Messages",
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = textColor
|
||||
)
|
||||
Text(
|
||||
text = "Your personal cloud storage",
|
||||
fontSize = 14.sp,
|
||||
color = secondaryTextColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(start = 84.dp),
|
||||
color = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatItem(
|
||||
chat: Chat,
|
||||
isDarkTheme: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
|
||||
val (avatarTextColor, avatarBgColor) = getAvatarColor(chat.name, isDarkTheme)
|
||||
val initials = getInitials(chat.name)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Avatar
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(56.dp)
|
||||
.clip(CircleShape)
|
||||
.background(avatarBgColor),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = initials,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = avatarTextColor
|
||||
)
|
||||
|
||||
// Online indicator
|
||||
if (chat.isOnline) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.size(14.dp)
|
||||
.clip(CircleShape)
|
||||
.background(if (isDarkTheme) Color(0xFF1E1E1E) else Color.White)
|
||||
.padding(2.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF4CAF50))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = chat.name,
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = formatTime(chat.lastMessageTime),
|
||||
fontSize = 13.sp,
|
||||
color = if (chat.unreadCount > 0) PrimaryBlue else secondaryTextColor
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = chat.lastMessage,
|
||||
fontSize = 15.sp,
|
||||
color = secondaryTextColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
if (chat.unreadCount > 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp)
|
||||
.clip(CircleShape)
|
||||
.background(PrimaryBlue)
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = if (chat.unreadCount > 99) "99+" else chat.unreadCount.toString(),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(start = 84.dp),
|
||||
color = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyChatsState(
|
||||
isDarkTheme: Boolean,
|
||||
onNewChat: () -> Unit
|
||||
) {
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(60.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.clip(CircleShape)
|
||||
.background(PrimaryBlue.copy(alpha = 0.1f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ChatBubbleOutline,
|
||||
contentDescription = null,
|
||||
tint = PrimaryBlue,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = "No chats yet",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = textColor
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Start a conversation with\nsomeone new",
|
||||
fontSize = 15.sp,
|
||||
color = secondaryTextColor,
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Button(
|
||||
onClick = onNewChat,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PrimaryBlue,
|
||||
contentColor = Color.White
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Start Chat", fontSize = 16.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatTime(date: Date): String {
|
||||
val now = Calendar.getInstance()
|
||||
val messageTime = Calendar.getInstance().apply { time = date }
|
||||
|
||||
return when {
|
||||
// Today
|
||||
now.get(Calendar.DATE) == messageTime.get(Calendar.DATE) -> {
|
||||
SimpleDateFormat("HH:mm", Locale.getDefault()).format(date)
|
||||
}
|
||||
// Yesterday
|
||||
now.get(Calendar.DATE) - messageTime.get(Calendar.DATE) == 1 -> {
|
||||
"Yesterday"
|
||||
}
|
||||
// This week
|
||||
now.get(Calendar.WEEK_OF_YEAR) == messageTime.get(Calendar.WEEK_OF_YEAR) -> {
|
||||
SimpleDateFormat("EEE", Locale.getDefault()).format(date)
|
||||
}
|
||||
// This year
|
||||
now.get(Calendar.YEAR) == messageTime.get(Calendar.YEAR) -> {
|
||||
SimpleDateFormat("MMM d", Locale.getDefault()).format(date)
|
||||
}
|
||||
// Other
|
||||
else -> {
|
||||
SimpleDateFormat("dd.MM.yy", Locale.getDefault()).format(date)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerDefaults
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@@ -238,12 +239,23 @@ fun OnboardingScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Pager for text content
|
||||
// Pager for text content with easier swipes
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(150.dp)
|
||||
.height(150.dp),
|
||||
flingBehavior = PagerDefaults.flingBehavior(
|
||||
state = pagerState,
|
||||
lowVelocityAnimationSpec = tween(
|
||||
durationMillis = 300,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
snapAnimationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
)
|
||||
)
|
||||
) { page ->
|
||||
OnboardingPageContent(
|
||||
page = onboardingPages[page],
|
||||
@@ -381,14 +393,44 @@ fun AnimatedRosettaLogo(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// Pre-render all animations to avoid lag
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
// Rosetta icon (page 0)
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
// Rosetta icon (page 0) with pulse animation
|
||||
if (currentPage == 0) {
|
||||
val pulseScale by rememberInfiniteTransition(label = "pulse").animateFloat(
|
||||
initialValue = 1f,
|
||||
targetValue = 1.08f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(1000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "pulseScale"
|
||||
)
|
||||
|
||||
// Glow effect behind logo - separate Box without clipping
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(200.dp)
|
||||
.scale(pulseScale)
|
||||
.background(
|
||||
color = Color(0xFF54A9EB).copy(alpha = 0.15f),
|
||||
shape = CircleShape
|
||||
)
|
||||
)
|
||||
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rosetta_icon),
|
||||
contentDescription = "Rosetta Logo",
|
||||
modifier = Modifier
|
||||
.size(180.dp)
|
||||
.scale(pulseScale)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
}
|
||||
painter = painterResource(id = R.drawable.rosetta_icon),
|
||||
contentDescription = "Rosetta Logo",
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.scale(pulseScale)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user