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.AccountManager
|
||||||
import com.rosetta.messenger.data.DecryptedAccount
|
import com.rosetta.messenger.data.DecryptedAccount
|
||||||
import com.rosetta.messenger.data.PreferencesManager
|
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.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.onboarding.OnboardingScreen
|
||||||
import com.rosetta.messenger.ui.splash.SplashScreen
|
import com.rosetta.messenger.ui.splash.SplashScreen
|
||||||
import com.rosetta.messenger.ui.theme.RosettaAndroidTheme
|
import com.rosetta.messenger.ui.theme.RosettaAndroidTheme
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private lateinit var preferencesManager: PreferencesManager
|
private lateinit var preferencesManager: PreferencesManager
|
||||||
@@ -49,11 +53,31 @@ class MainActivity : ComponentActivity() {
|
|||||||
var showOnboarding by remember { mutableStateOf(true) }
|
var showOnboarding by remember { mutableStateOf(true) }
|
||||||
var hasExistingAccount by remember { mutableStateOf<Boolean?>(null) }
|
var hasExistingAccount by remember { mutableStateOf<Boolean?>(null) }
|
||||||
var currentAccount by remember { mutableStateOf<DecryptedAccount?>(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) {
|
LaunchedEffect(Unit) {
|
||||||
val accounts = accountManager.getAllAccounts()
|
val accounts = accountManager.getAllAccounts()
|
||||||
hasExistingAccount = accounts.isNotEmpty()
|
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
|
// Wait for initial load
|
||||||
@@ -112,15 +136,46 @@ class MainActivity : ComponentActivity() {
|
|||||||
AuthFlow(
|
AuthFlow(
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
hasExistingAccount = screen == "auth_unlock",
|
hasExistingAccount = screen == "auth_unlock",
|
||||||
|
accounts = accountInfoList,
|
||||||
onAuthComplete = { account ->
|
onAuthComplete = { account ->
|
||||||
currentAccount = account
|
currentAccount = account
|
||||||
hasExistingAccount = true
|
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" -> {
|
"main" -> {
|
||||||
MainScreen(
|
MainScreen(
|
||||||
account = currentAccount,
|
account = currentAccount,
|
||||||
|
isDarkTheme = isDarkTheme,
|
||||||
onLogout = {
|
onLogout = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
accountManager.logout()
|
accountManager.logout()
|
||||||
@@ -140,17 +195,54 @@ class MainActivity : ComponentActivity() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
account: DecryptedAccount? = null,
|
account: DecryptedAccount? = null,
|
||||||
|
isDarkTheme: Boolean = true,
|
||||||
onLogout: () -> Unit = {}
|
onLogout: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Box(
|
// Demo chats for now
|
||||||
modifier = Modifier.fillMaxSize(),
|
val demoChats = remember {
|
||||||
contentAlignment = Alignment.Center
|
listOf(
|
||||||
) {
|
Chat(
|
||||||
Text(
|
id = "1",
|
||||||
text = "Welcome to Rosetta! 🚀\n\nYou're logged in!",
|
name = "Alice Johnson",
|
||||||
fontSize = 24.sp,
|
lastMessage = "Hey! How are you doing?",
|
||||||
fontWeight = FontWeight.Bold,
|
lastMessageTime = Date(),
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
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
|
import com.rosetta.messenger.data.DecryptedAccount
|
||||||
|
|
||||||
enum class AuthScreen {
|
enum class AuthScreen {
|
||||||
|
SELECT_ACCOUNT,
|
||||||
WELCOME,
|
WELCOME,
|
||||||
SEED_PHRASE,
|
SEED_PHRASE,
|
||||||
CONFIRM_SEED,
|
CONFIRM_SEED,
|
||||||
@@ -18,12 +19,22 @@ enum class AuthScreen {
|
|||||||
fun AuthFlow(
|
fun AuthFlow(
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
hasExistingAccount: Boolean,
|
hasExistingAccount: Boolean,
|
||||||
onAuthComplete: (DecryptedAccount?) -> Unit
|
accounts: List<AccountInfo> = emptyList(),
|
||||||
|
onAuthComplete: (DecryptedAccount?) -> Unit,
|
||||||
|
onLogout: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
var currentScreen by remember {
|
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 seedPhrase by remember { mutableStateOf<List<String>>(emptyList()) }
|
||||||
|
var selectedAccountId by remember { mutableStateOf<String?>(accounts.firstOrNull()?.id) }
|
||||||
|
var showCreateModal by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = currentScreen,
|
targetState = currentScreen,
|
||||||
@@ -34,6 +45,29 @@ fun AuthFlow(
|
|||||||
label = "authScreenTransition"
|
label = "authScreenTransition"
|
||||||
) { screen ->
|
) { screen ->
|
||||||
when (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 -> {
|
AuthScreen.WELCOME -> {
|
||||||
WelcomeScreen(
|
WelcomeScreen(
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
@@ -45,7 +79,11 @@ fun AuthFlow(
|
|||||||
AuthScreen.SEED_PHRASE -> {
|
AuthScreen.SEED_PHRASE -> {
|
||||||
SeedPhraseScreen(
|
SeedPhraseScreen(
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
onBack = { currentScreen = AuthScreen.WELCOME },
|
onBack = {
|
||||||
|
currentScreen = if (hasExistingAccount && accounts.size > 1)
|
||||||
|
AuthScreen.SELECT_ACCOUNT
|
||||||
|
else AuthScreen.WELCOME
|
||||||
|
},
|
||||||
onConfirm = { words ->
|
onConfirm = { words ->
|
||||||
seedPhrase = words
|
seedPhrase = words
|
||||||
currentScreen = AuthScreen.CONFIRM_SEED
|
currentScreen = AuthScreen.CONFIRM_SEED
|
||||||
@@ -74,7 +112,11 @@ fun AuthFlow(
|
|||||||
AuthScreen.IMPORT_SEED -> {
|
AuthScreen.IMPORT_SEED -> {
|
||||||
ImportSeedPhraseScreen(
|
ImportSeedPhraseScreen(
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
onBack = { currentScreen = AuthScreen.WELCOME },
|
onBack = {
|
||||||
|
currentScreen = if (hasExistingAccount && accounts.size > 1)
|
||||||
|
AuthScreen.SELECT_ACCOUNT
|
||||||
|
else AuthScreen.WELCOME
|
||||||
|
},
|
||||||
onSeedPhraseImported = { words ->
|
onSeedPhraseImported = { words ->
|
||||||
seedPhrase = words
|
seedPhrase = words
|
||||||
currentScreen = AuthScreen.SET_PASSWORD
|
currentScreen = AuthScreen.SET_PASSWORD
|
||||||
@@ -85,7 +127,13 @@ fun AuthFlow(
|
|||||||
AuthScreen.UNLOCK -> {
|
AuthScreen.UNLOCK -> {
|
||||||
UnlockScreen(
|
UnlockScreen(
|
||||||
isDarkTheme = isDarkTheme,
|
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.animation.core.*
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.layout.*
|
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.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
@@ -17,19 +13,33 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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.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.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.rosetta.messenger.crypto.CryptoManager
|
|
||||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
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
|
@Composable
|
||||||
fun ConfirmSeedPhraseScreen(
|
fun ConfirmSeedPhraseScreen(
|
||||||
seedPhrase: List<String>,
|
seedPhrase: List<String>,
|
||||||
@@ -37,18 +47,16 @@ fun ConfirmSeedPhraseScreen(
|
|||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onConfirmed: () -> Unit
|
onConfirmed: () -> Unit
|
||||||
) {
|
) {
|
||||||
val themeAnimSpec = tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f))
|
val backgroundColor = if (isDarkTheme) Color(0xFF1E1E1E) else Color(0xFFFFFFFF)
|
||||||
val backgroundColor by animateColorAsState(if (isDarkTheme) AuthBackground else AuthBackgroundLight, animationSpec = themeAnimSpec)
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val textColor by animateColorAsState(if (isDarkTheme) Color.White else Color.Black, animationSpec = themeAnimSpec)
|
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||||
val secondaryTextColor by animateColorAsState(if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666), animationSpec = themeAnimSpec)
|
|
||||||
val cardColor by animateColorAsState(if (isDarkTheme) AuthSurface else AuthSurfaceLight, animationSpec = themeAnimSpec)
|
|
||||||
|
|
||||||
// Select 4 random words to confirm
|
// Select 4 words at fixed positions to confirm (2, 5, 9, 12)
|
||||||
val wordsToConfirm = remember {
|
val wordsToConfirm = remember {
|
||||||
listOf(1, 4, 8, 11).map { index -> index to seedPhrase[index] }
|
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 showError by remember { mutableStateOf(false) }
|
||||||
var visible by remember { mutableStateOf(false) }
|
var visible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -56,43 +64,33 @@ fun ConfirmSeedPhraseScreen(
|
|||||||
visible = true
|
visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val allCorrect = wordsToConfirm.mapIndexed { i, (_, word) ->
|
// Check if the 4 confirmation words are correct
|
||||||
userInputs[i].trim().lowercase() == word.lowercase()
|
val allCorrect = wordsToConfirm.all { (index, word) ->
|
||||||
}.all { it }
|
userInputs[index].trim().lowercase() == word.lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all 4 words have input
|
||||||
|
val allFilled = wordsToConfirm.all { (index, _) ->
|
||||||
|
userInputs[index].isNotBlank()
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
|
.statusBarsPadding()
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
modifier = Modifier
|
// Top bar
|
||||||
.fillMaxSize()
|
|
||||||
.statusBarsPadding()
|
|
||||||
) {
|
|
||||||
// Top Bar
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
.padding(4.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(
|
Icon(Icons.Default.ArrowBack, "Back", tint = textColor)
|
||||||
imageVector = Icons.Default.ArrowBack,
|
|
||||||
contentDescription = "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(
|
Column(
|
||||||
@@ -104,82 +102,127 @@ fun ConfirmSeedPhraseScreen(
|
|||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// Info Card
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn(tween(500)) + slideInVertically(
|
enter = fadeIn(tween(500)) + slideInVertically(
|
||||||
initialOffsetY = { -30 },
|
initialOffsetY = { -20 },
|
||||||
animationSpec = tween(500)
|
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(
|
||||||
text = "Enter the following words from your seed phrase to confirm you've backed it up correctly.",
|
text = "Confirm Backup",
|
||||||
fontSize = 14.sp,
|
fontSize = 28.sp,
|
||||||
color = secondaryTextColor,
|
fontWeight = FontWeight.Bold,
|
||||||
lineHeight = 18.sp
|
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))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
// Word inputs
|
// Two column layout like SeedPhraseScreen
|
||||||
wordsToConfirm.forEachIndexed { index, (wordIndex, _) ->
|
AnimatedVisibility(
|
||||||
val isCorrect = userInputs[index].trim().lowercase() ==
|
visible = visible,
|
||||||
wordsToConfirm[index].second.lowercase()
|
enter = fadeIn(tween(500, delayMillis = 200))
|
||||||
val hasInput = userInputs[index].isNotBlank()
|
) {
|
||||||
|
Row(
|
||||||
AnimatedVisibility(
|
modifier = Modifier.fillMaxWidth(),
|
||||||
visible = visible,
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
enter = fadeIn(tween(500, delayMillis = 100 + (index * 100))) + slideInVertically(
|
|
||||||
initialOffsetY = { 50 },
|
|
||||||
animationSpec = tween(500, delayMillis = 100 + (index * 100))
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
WordInputField(
|
// Left column (words 1-6)
|
||||||
wordNumber = wordIndex + 1,
|
Column(
|
||||||
value = userInputs[index],
|
modifier = Modifier.weight(1f),
|
||||||
onValueChange = {
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
userInputs = userInputs.toMutableList().apply {
|
) {
|
||||||
this[index] = it
|
for (i in 0..5) {
|
||||||
}
|
val needsInput = wordsToConfirm.any { it.first == i }
|
||||||
showError = false
|
val correctWord = seedPhrase[i]
|
||||||
},
|
|
||||||
isCorrect = if (hasInput) isCorrect else null,
|
AnimatedConfirmWordItem(
|
||||||
isDarkTheme = isDarkTheme
|
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
|
// Error message
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = showError,
|
visible = showError,
|
||||||
enter = fadeIn() + slideInVertically { -10 },
|
enter = fadeIn(tween(300)) + scaleIn(initialScale = 0.9f),
|
||||||
exit = fadeOut()
|
exit = fadeOut(tween(200))
|
||||||
) {
|
) {
|
||||||
Text(
|
Column {
|
||||||
text = "Some words don't match. Please check and try again.",
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
fontSize = 14.sp,
|
Text(
|
||||||
color = Color(0xFFE53935),
|
text = "Some words don't match. Please check and try again.",
|
||||||
textAlign = TextAlign.Center
|
fontSize = 14.sp,
|
||||||
)
|
color = Color(0xFFE53935),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
@@ -187,90 +230,208 @@ fun ConfirmSeedPhraseScreen(
|
|||||||
// Continue Button
|
// Continue Button
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn(tween(500, delayMillis = 500)) + slideInVertically(
|
enter = fadeIn(tween(500, delayMillis = 600)) + slideInVertically(
|
||||||
initialOffsetY = { 50 },
|
initialOffsetY = { 50 },
|
||||||
animationSpec = tween(500, delayMillis = 500)
|
animationSpec = tween(500, delayMillis = 600)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (allCorrect) {
|
if (allCorrect) {
|
||||||
onConfirmed()
|
onConfirmed()
|
||||||
} else {
|
} else {
|
||||||
showError = true
|
showError = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
enabled = allFilled,
|
||||||
.fillMaxWidth()
|
modifier = Modifier
|
||||||
.height(56.dp),
|
.fillMaxWidth()
|
||||||
colors = ButtonDefaults.buttonColors(
|
.height(50.dp),
|
||||||
containerColor = if (allCorrect) PrimaryBlue else PrimaryBlue.copy(alpha = 0.5f)
|
colors = ButtonDefaults.buttonColors(
|
||||||
),
|
containerColor = PrimaryBlue,
|
||||||
shape = RoundedCornerShape(12.dp)
|
contentColor = Color.White,
|
||||||
) {
|
disabledContainerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8),
|
||||||
Text(
|
disabledContentColor = if (isDarkTheme) Color(0xFF666666) else Color(0xFF999999)
|
||||||
text = "Confirm",
|
),
|
||||||
fontSize = 16.sp,
|
shape = RoundedCornerShape(12.dp)
|
||||||
fontWeight = FontWeight.SemiBold
|
) {
|
||||||
)
|
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
|
@Composable
|
||||||
private fun WordInputField(
|
private fun AnimatedConfirmWordItem(
|
||||||
wordNumber: Int,
|
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,
|
value: String,
|
||||||
onValueChange: (String) -> Unit,
|
onValueChange: (String) -> Unit,
|
||||||
isCorrect: Boolean?,
|
isCorrect: Boolean?,
|
||||||
isDarkTheme: Boolean
|
isDarkTheme: Boolean,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val numberColor = if (isDarkTheme) Color(0xFF888888) else Color(0xFF999999)
|
||||||
val labelColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
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) {
|
val borderColor = when (isCorrect) {
|
||||||
true -> Color(0xFF4CAF50)
|
true -> Color(0xFF4CAF50) // Green
|
||||||
false -> Color(0xFFE53935)
|
false -> Color(0xFFE53935) // Red
|
||||||
null -> if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD0D0D0)
|
null -> if (isFocused) PrimaryBlue else Color.Transparent
|
||||||
}
|
}
|
||||||
|
|
||||||
val trailingIcon: @Composable (() -> Unit)? = when (isCorrect) {
|
Box(
|
||||||
true -> {
|
modifier = modifier
|
||||||
{ Icon(Icons.Default.Check, null, tint = Color(0xFF4CAF50)) }
|
.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.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -35,12 +36,10 @@ fun ImportSeedPhraseScreen(
|
|||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1E1E1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1E1E1E) else Color(0xFFFFFFFF)
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
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 words by remember { mutableStateOf(List(12) { "" }) }
|
||||||
var error by remember { mutableStateOf<String?>(null) }
|
var error by remember { mutableStateOf<String?>(null) }
|
||||||
var pastedText by remember { mutableStateOf("") }
|
|
||||||
var showPasteDialog by remember { mutableStateOf(false) }
|
|
||||||
var visible by remember { mutableStateOf(false) }
|
var visible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -49,18 +48,6 @@ fun ImportSeedPhraseScreen(
|
|||||||
|
|
||||||
val allWordsFilled = words.all { it.isNotBlank() }
|
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(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -68,7 +55,7 @@ fun ImportSeedPhraseScreen(
|
|||||||
.statusBarsPadding()
|
.statusBarsPadding()
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
// Simple top bar
|
// Top bar
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -80,17 +67,13 @@ fun ImportSeedPhraseScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
Column(
|
||||||
visible = visible,
|
modifier = Modifier
|
||||||
enter = fadeIn(tween(500, easing = FastOutSlowInEasing))
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Column(
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 24.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
@@ -111,98 +94,140 @@ fun ImportSeedPhraseScreen(
|
|||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn(tween(500, delayMillis = 100))
|
enter = fadeIn(tween(500, delayMillis = 100)) + slideInVertically(
|
||||||
|
initialOffsetY = { -20 },
|
||||||
|
animationSpec = tween(500, delayMillis = 100)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Enter your 12-word recovery phrase",
|
text = "Enter your 12-word recovery phrase\nto restore your account.",
|
||||||
fontSize = 15.sp,
|
fontSize = 15.sp,
|
||||||
color = secondaryTextColor,
|
color = secondaryTextColor,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center,
|
||||||
|
lineHeight = 22.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// Paste button
|
// Paste button - directly reads from clipboard
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn(tween(500, delayMillis = 200)) + scaleIn(
|
enter = fadeIn(tween(500, delayMillis = 150)) + scaleIn(
|
||||||
initialScale = 0.9f,
|
initialScale = 0.9f,
|
||||||
animationSpec = tween(500, delayMillis = 200)
|
animationSpec = tween(500, delayMillis = 150)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Button(
|
TextButton(
|
||||||
onClick = { showPasteDialog = true },
|
onClick = {
|
||||||
modifier = Modifier
|
val clipboardText = clipboardManager.getText()?.text
|
||||||
.fillMaxWidth()
|
if (clipboardText != null && clipboardText.isNotBlank()) {
|
||||||
.height(50.dp),
|
val parsed = clipboardText.trim().lowercase()
|
||||||
colors = ButtonDefaults.buttonColors(
|
.split(Regex("[\\s,;]+"))
|
||||||
containerColor = PrimaryBlue,
|
.filter { it.isNotBlank() }
|
||||||
contentColor = Color.White
|
.map { it.trim() }
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
when {
|
||||||
) {
|
parsed.size == 12 -> {
|
||||||
Icon(
|
words = parsed.toList()
|
||||||
Icons.Default.ContentPaste,
|
error = null
|
||||||
contentDescription = null,
|
}
|
||||||
modifier = Modifier.size(22.dp)
|
parsed.size > 12 -> {
|
||||||
)
|
words = parsed.take(12).toList()
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
error = null
|
||||||
Text(
|
}
|
||||||
"Paste All 12 Words",
|
parsed.isNotEmpty() -> {
|
||||||
fontSize = 16.sp,
|
error = "Clipboard contains ${parsed.size} words, need 12"
|
||||||
fontWeight = FontWeight.SemiBold
|
}
|
||||||
)
|
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(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn(tween(500, delayMillis = 300))
|
enter = fadeIn(tween(500, delayMillis = 200))
|
||||||
) {
|
) {
|
||||||
Column(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
.clip(RoundedCornerShape(12.dp))
|
) {
|
||||||
.background(cardBackground)
|
// Left column (words 1-6)
|
||||||
.padding(16.dp),
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
modifier = Modifier.weight(1f),
|
||||||
) {
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
for (row in 0..3) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
) {
|
||||||
for (col in 0..2) {
|
for (i in 0..5) {
|
||||||
val index = row * 3 + col
|
AnimatedWordInputItem(
|
||||||
WordInputItem(
|
number = i + 1,
|
||||||
number = index + 1,
|
value = words[i],
|
||||||
value = words[index],
|
|
||||||
onValueChange = { newValue ->
|
onValueChange = { newValue ->
|
||||||
words = words.toMutableList().apply {
|
words = words.toMutableList().apply {
|
||||||
this[index] = newValue.lowercase().trim()
|
this[i] = newValue.lowercase().trim()
|
||||||
}
|
}
|
||||||
error = null
|
error = null
|
||||||
},
|
},
|
||||||
isDarkTheme = isDarkTheme,
|
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
|
// Error
|
||||||
if (error != null) {
|
AnimatedVisibility(
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
visible = error != null,
|
||||||
AnimatedVisibility(
|
enter = fadeIn(tween(300)) + scaleIn(initialScale = 0.9f),
|
||||||
visible = true,
|
exit = fadeOut(tween(200))
|
||||||
enter = fadeIn(tween(300)) + scaleIn(initialScale = 0.9f)
|
) {
|
||||||
) {
|
Column {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
text = error ?: "",
|
text = error ?: "",
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
@@ -217,95 +242,80 @@ fun ImportSeedPhraseScreen(
|
|||||||
// Import button
|
// Import button
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn(tween(500, delayMillis = 400)) + slideInVertically(
|
enter = fadeIn(tween(500, delayMillis = 600)) + slideInVertically(
|
||||||
initialOffsetY = { 50 },
|
initialOffsetY = { 50 },
|
||||||
animationSpec = tween(500, delayMillis = 400)
|
animationSpec = tween(500, delayMillis = 600)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
val seedPhrase = words.map { it.trim() }
|
val seedPhrase = words.map { it.trim() }
|
||||||
|
|
||||||
if (seedPhrase.any { it.isBlank() }) {
|
if (seedPhrase.any { it.isBlank() }) {
|
||||||
error = "Please fill in all words"
|
error = "Please fill in all words"
|
||||||
return@Button
|
return@Button
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!CryptoManager.validateSeedPhrase(seedPhrase)) {
|
if (!CryptoManager.validateSeedPhrase(seedPhrase)) {
|
||||||
error = "Invalid recovery phrase"
|
error = "Invalid recovery phrase"
|
||||||
return@Button
|
return@Button
|
||||||
}
|
}
|
||||||
|
|
||||||
onSeedPhraseImported(seedPhrase)
|
onSeedPhraseImported(seedPhrase)
|
||||||
},
|
},
|
||||||
enabled = allWordsFilled,
|
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") },
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(120.dp),
|
.height(50.dp),
|
||||||
colors = OutlinedTextFieldDefaults.colors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
focusedBorderColor = PrimaryBlue,
|
containerColor = PrimaryBlue,
|
||||||
cursorColor = PrimaryBlue
|
contentColor = Color.White,
|
||||||
|
disabledContainerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8),
|
||||||
|
disabledContentColor = if (isDarkTheme) Color(0xFF666666) else Color(0xFF999999)
|
||||||
),
|
),
|
||||||
shape = RoundedCornerShape(12.dp)
|
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 }) {
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
Text("Cancel")
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
containerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color.White,
|
|
||||||
shape = RoundedCornerShape(20.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
|
@Composable
|
||||||
private fun WordInputItem(
|
private fun WordInputItem(
|
||||||
number: Int,
|
number: Int,
|
||||||
@@ -314,55 +324,81 @@ private fun WordInputItem(
|
|||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val itemBg = if (isDarkTheme) Color(0xFF1E1E1E) else Color.White
|
val numberColor = if (isDarkTheme) Color(0xFF888888) else Color(0xFF999999)
|
||||||
val numberColor = if (isDarkTheme) Color(0xFF666666) else Color(0xFFAAAAAA)
|
val hintColor = if (isDarkTheme) Color(0xFF555555) else Color(0xFFBBBBBB)
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val bgColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
|
||||||
val hintColor = if (isDarkTheme) Color(0xFF666666) else Color(0xFFAAAAAA)
|
|
||||||
|
|
||||||
var isFocused by remember { mutableStateOf(false) }
|
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
|
modifier = modifier
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.fillMaxWidth()
|
||||||
.border(1.dp, borderColor, RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(12.dp))
|
||||||
.background(itemBg)
|
.border(
|
||||||
.padding(horizontal = 10.dp, vertical = 10.dp),
|
width = if (isFocused) 2.dp else 0.dp,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
color = if (isFocused) PrimaryBlue else Color.Transparent,
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
.background(bgColor)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 14.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Row(
|
||||||
text = "$number.",
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
fontSize = 13.sp,
|
modifier = Modifier.fillMaxWidth()
|
||||||
color = numberColor,
|
) {
|
||||||
modifier = Modifier.width(22.dp)
|
Text(
|
||||||
)
|
text = "$number.",
|
||||||
BasicTextField(
|
|
||||||
value = value,
|
|
||||||
onValueChange = onValueChange,
|
|
||||||
textStyle = TextStyle(
|
|
||||||
fontSize = 15.sp,
|
fontSize = 15.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
color = numberColor,
|
||||||
color = textColor,
|
modifier = Modifier.width(28.dp)
|
||||||
fontFamily = FontFamily.Monospace
|
)
|
||||||
),
|
BasicTextField(
|
||||||
singleLine = true,
|
value = value,
|
||||||
cursorBrush = SolidColor(PrimaryBlue),
|
onValueChange = onValueChange,
|
||||||
modifier = Modifier
|
textStyle = TextStyle(
|
||||||
.weight(1f)
|
fontSize = 17.sp,
|
||||||
.onFocusChanged { isFocused = it.isFocused },
|
fontWeight = FontWeight.SemiBold,
|
||||||
decorationBox = { innerTextField ->
|
color = if (value.isNotEmpty()) wordColor else hintColor,
|
||||||
Box {
|
fontFamily = FontFamily.Monospace
|
||||||
if (value.isEmpty()) {
|
),
|
||||||
Text(
|
singleLine = true,
|
||||||
"word",
|
cursorBrush = SolidColor(PrimaryBlue),
|
||||||
fontSize = 15.sp,
|
modifier = Modifier
|
||||||
color = hintColor.copy(alpha = 0.5f),
|
.weight(1f)
|
||||||
fontFamily = FontFamily.Monospace
|
.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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -248,31 +249,52 @@ private fun WordItem(
|
|||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val itemBg = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
|
val numberColor = if (isDarkTheme) Color(0xFF888888) else Color(0xFF999999)
|
||||||
val numberColor = if (isDarkTheme) Color(0xFF666666) else Color(0xFFAAAAAA)
|
|
||||||
val wordColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
|
|
||||||
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
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.clip(RoundedCornerShape(12.dp))
|
||||||
.background(itemBg)
|
.background(bgColor)
|
||||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
.padding(horizontal = 16.dp, vertical = 14.dp)
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Row(
|
||||||
text = "$number.",
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
fontSize = 15.sp,
|
modifier = Modifier.fillMaxWidth()
|
||||||
color = numberColor,
|
) {
|
||||||
modifier = Modifier.width(28.dp)
|
Text(
|
||||||
)
|
text = "$number.",
|
||||||
Text(
|
fontSize = 15.sp,
|
||||||
text = word,
|
color = numberColor,
|
||||||
fontSize = 17.sp,
|
modifier = Modifier.width(28.dp)
|
||||||
fontWeight = FontWeight.SemiBold,
|
)
|
||||||
color = wordColor,
|
Text(
|
||||||
fontFamily = FontFamily.Monospace
|
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
|
@Composable
|
||||||
fun UnlockScreen(
|
fun UnlockScreen(
|
||||||
isDarkTheme: Boolean,
|
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 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 backgroundColor by animateColorAsState(if (isDarkTheme) AuthBackground else AuthBackgroundLight, animationSpec = themeAnimSpec)
|
||||||
@@ -56,7 +58,7 @@ fun UnlockScreen(
|
|||||||
|
|
||||||
// Load current account
|
// Load current account
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
currentPublicKey = accountManager.currentPublicKey.first()
|
currentPublicKey = selectedAccountId ?: accountManager.currentPublicKey.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entry animation
|
// 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))
|
Spacer(modifier = Modifier.weight(0.3f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ fun WelcomeScreen(
|
|||||||
val lockComposition by rememberLottieComposition(LottieCompositionSpec.Asset("lottie/lock.json"))
|
val lockComposition by rememberLottieComposition(LottieCompositionSpec.Asset("lottie/lock.json"))
|
||||||
val lockProgress by animateLottieCompositionAsState(
|
val lockProgress by animateLottieCompositionAsState(
|
||||||
composition = lockComposition,
|
composition = lockComposition,
|
||||||
iterations = LottieConstants.IterateForever
|
iterations = 1, // Play once
|
||||||
|
speed = 1f
|
||||||
)
|
)
|
||||||
|
|
||||||
// Entry animation
|
// 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.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.PagerDefaults
|
||||||
import androidx.compose.foundation.pager.PagerState
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
@@ -238,12 +239,23 @@ fun OnboardingScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
// Pager for text content
|
// Pager for text content with easier swipes
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.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 ->
|
) { page ->
|
||||||
OnboardingPageContent(
|
OnboardingPageContent(
|
||||||
page = onboardingPages[page],
|
page = onboardingPages[page],
|
||||||
@@ -381,14 +393,44 @@ fun AnimatedRosettaLogo(
|
|||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
// Pre-render all animations to avoid lag
|
// Pre-render all animations to avoid lag
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
// Rosetta icon (page 0)
|
// Rosetta icon (page 0) with pulse animation
|
||||||
if (currentPage == 0) {
|
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(
|
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),
|
painter = painterResource(id = R.drawable.rosetta_icon),
|
||||||
contentDescription = "Rosetta Logo",
|
contentDescription = "Rosetta Logo",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.scale(pulseScale)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user