feat: Enhance UnlockScreen with account selection dropdown and search functionality

This commit is contained in:
k1ngsterr1
2026-01-09 02:48:34 +05:00
parent e29a851510
commit d3b25ae64f
5 changed files with 404 additions and 160 deletions

View File

@@ -201,7 +201,7 @@ fun MainScreen(
onToggleTheme: () -> Unit = {},
onLogout: () -> Unit = {}
) {
val accountName = account?.name ?: "Rosetta User"
val accountName = account?.publicKey ?: "04c266b98ae5"
val accountPhone = account?.publicKey?.take(16)?.let {
"+${it.take(1)} ${it.substring(1, 4)} ${it.substring(4, 7)}${it.substring(7)}"
} ?: "+7 775 9932587"

View File

@@ -130,9 +130,9 @@ fun AuthFlow(
selectedAccountId = selectedAccountId,
onUnlocked = { account -> onAuthComplete(account) },
onSwitchAccount = {
if (accounts.size > 1) {
currentScreen = AuthScreen.SELECT_ACCOUNT
}
// Navigate to create new account screen
currentScreen = AuthScreen.WELCOME
selectedAccountId = null
}
)
}

View File

@@ -58,6 +58,24 @@ fun SetPasswordScreen(
var error by remember { mutableStateOf<String?>(null) }
var visible by remember { mutableStateOf(false) }
// Track keyboard visibility
val view = androidx.compose.ui.platform.LocalView.current
var isKeyboardVisible by remember { mutableStateOf(false) }
DisposableEffect(view) {
val listener = android.view.ViewTreeObserver.OnGlobalLayoutListener {
val rect = android.graphics.Rect()
view.getWindowVisibleDisplayFrame(rect)
val screenHeight = view.rootView.height
val keypadHeight = screenHeight - rect.bottom
isKeyboardVisible = keypadHeight > screenHeight * 0.15
}
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
onDispose {
view.viewTreeObserver.removeOnGlobalLayoutListener(listener)
}
}
LaunchedEffect(Unit) {
visible = true
}
@@ -109,9 +127,18 @@ fun SetPasswordScreen(
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 8.dp else 16.dp))
// Lock Icon - smaller when keyboard is visible
val iconSize by animateDpAsState(
targetValue = if (isKeyboardVisible) 48.dp else 80.dp,
animationSpec = tween(300, easing = FastOutSlowInEasing)
)
val iconInnerSize by animateDpAsState(
targetValue = if (isKeyboardVisible) 24.dp else 40.dp,
animationSpec = tween(300, easing = FastOutSlowInEasing)
)
// Lock Icon
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500)) + scaleIn(
@@ -121,8 +148,8 @@ fun SetPasswordScreen(
) {
Box(
modifier = Modifier
.size(80.dp)
.clip(RoundedCornerShape(20.dp))
.size(iconSize)
.clip(RoundedCornerShape(if (isKeyboardVisible) 12.dp else 20.dp))
.background(PrimaryBlue.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
@@ -130,12 +157,12 @@ fun SetPasswordScreen(
Icons.Default.Lock,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(40.dp)
modifier = Modifier.size(iconInnerSize)
)
}
}
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 12.dp else 24.dp))
AnimatedVisibility(
visible = visible,
@@ -146,13 +173,13 @@ fun SetPasswordScreen(
) {
Text(
text = "Protect Your Account",
fontSize = 24.sp,
fontSize = if (isKeyboardVisible) 20.sp else 24.sp,
fontWeight = FontWeight.Bold,
color = textColor
)
}
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 6.dp else 8.dp))
AnimatedVisibility(
visible = visible,
@@ -160,14 +187,14 @@ fun SetPasswordScreen(
) {
Text(
text = "This password encrypts your keys locally.\nYou'll need it to unlock Rosetta.",
fontSize = 14.sp,
fontSize = if (isKeyboardVisible) 12.sp else 14.sp,
color = secondaryTextColor,
textAlign = TextAlign.Center,
lineHeight = 20.sp
lineHeight = if (isKeyboardVisible) 16.sp else 20.sp
)
}
Spacer(modifier = Modifier.height(32.dp))
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 16.dp else 32.dp))
// Password Field
AnimatedVisibility(

View File

@@ -5,6 +5,8 @@ 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.foundation.text.KeyboardOptions
@@ -15,7 +17,11 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
@@ -24,18 +30,28 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
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.R
import com.rosetta.messenger.crypto.CryptoManager
import com.rosetta.messenger.data.AccountManager
import com.rosetta.messenger.data.DecryptedAccount
import com.rosetta.messenger.data.EncryptedAccount
import com.rosetta.messenger.network.ProtocolManager
import com.rosetta.messenger.network.ProtocolState
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import com.rosetta.messenger.ui.chats.getAvatarColor
import com.rosetta.messenger.ui.chats.getAvatarText
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
// Account model for dropdown
data class AccountItem(
val publicKey: String,
val name: String,
val encryptedAccount: EncryptedAccount
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun UnlockScreen(
@@ -48,6 +64,7 @@ fun UnlockScreen(
val backgroundColor by animateColorAsState(if (isDarkTheme) AuthBackground else AuthBackgroundLight, animationSpec = themeAnimSpec)
val textColor by animateColorAsState(if (isDarkTheme) Color.White else Color.Black, animationSpec = themeAnimSpec)
val secondaryTextColor by animateColorAsState(if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666), animationSpec = themeAnimSpec)
val cardBackground by animateColorAsState(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5), animationSpec = themeAnimSpec)
val context = LocalContext.current
val accountManager = remember { AccountManager(context) }
@@ -57,17 +74,63 @@ fun UnlockScreen(
var passwordVisible by remember { mutableStateOf(false) }
var isUnlocking by remember { mutableStateOf(false) }
var error by remember { mutableStateOf<String?>(null) }
var currentPublicKey by remember { mutableStateOf<String?>(null) }
// Load current account
// Account selection state
var accounts by remember { mutableStateOf<List<AccountItem>>(emptyList()) }
var selectedAccount by remember { mutableStateOf<AccountItem?>(null) }
var isDropdownExpanded by remember { mutableStateOf(false) }
var searchQuery by remember { mutableStateOf("") }
val searchFocusRequester = remember { FocusRequester() }
// Load accounts
LaunchedEffect(Unit) {
currentPublicKey = selectedAccountId ?: accountManager.currentPublicKey.first()
val allAccounts = accountManager.getAllAccounts()
accounts = allAccounts.map { acc ->
AccountItem(
publicKey = acc.publicKey,
name = acc.name,
encryptedAccount = acc
)
}
// Select account
val targetPublicKey = selectedAccountId ?: accountManager.currentPublicKey.first()
selectedAccount = accounts.find { it.publicKey == targetPublicKey } ?: accounts.firstOrNull()
}
// Filter accounts by search
val filteredAccounts = remember(searchQuery, accounts) {
if (searchQuery.isEmpty()) accounts
else accounts.filter {
it.name.contains(searchQuery, ignoreCase = true) ||
it.publicKey.contains(searchQuery, ignoreCase = true)
}
}
// Entry animation
var visible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { visible = true }
// Dropdown animation
val dropdownProgress by animateFloatAsState(
targetValue = if (isDropdownExpanded) 1f else 0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
label = "dropdownProgress"
)
// Auto-focus search when dropdown opens
LaunchedEffect(isDropdownExpanded) {
if (isDropdownExpanded) {
kotlinx.coroutines.delay(200)
searchFocusRequester.requestFocus()
} else {
searchQuery = ""
}
}
Box(
modifier = Modifier
.fillMaxSize()
@@ -76,12 +139,13 @@ fun UnlockScreen(
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.imePadding()
.padding(horizontal = 24.dp)
.statusBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.weight(0.2f))
Spacer(modifier = Modifier.height(40.dp))
// Rosetta Logo
AnimatedVisibility(
@@ -92,12 +156,12 @@ fun UnlockScreen(
painter = painterResource(id = R.drawable.rosetta_icon),
contentDescription = "Rosetta",
modifier = Modifier
.size(120.dp)
.size(100.dp)
.clip(CircleShape)
)
}
Spacer(modifier = Modifier.height(32.dp))
Spacer(modifier = Modifier.height(24.dp))
// Title
AnimatedVisibility(
@@ -117,24 +181,263 @@ fun UnlockScreen(
Spacer(modifier = Modifier.height(8.dp))
// Account info
AnimatedVisibility(
visible = visible && currentPublicKey != null,
visible = visible,
enter = fadeIn(tween(600, delayMillis = 300))
) {
Text(
text = "Enter your password to unlock",
text = "Select your account and enter password",
fontSize = 16.sp,
color = secondaryTextColor,
textAlign = TextAlign.Center
)
}
Spacer(modifier = Modifier.height(48.dp))
Spacer(modifier = Modifier.height(32.dp))
// Account Selector Card
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600, delayMillis = 350)) + slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 350)
)
) {
Column {
// Account selector dropdown
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { isDropdownExpanded = !isDropdownExpanded },
colors = CardDefaults.cardColors(containerColor = cardBackground),
shape = RoundedCornerShape(16.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Avatar
if (selectedAccount != null) {
val avatarColors = getAvatarColor(selectedAccount!!.name, isDarkTheme)
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(avatarColors.backgroundColor),
contentAlignment = Alignment.Center
) {
Text(
text = getAvatarText(selectedAccount!!.publicKey),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = avatarColors.textColor
)
}
}
Spacer(modifier = Modifier.width(12.dp))
// Account info
Column(modifier = Modifier.weight(1f)) {
Text(
text = selectedAccount?.name ?: "Select Account",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (selectedAccount != null) {
Text(
text = selectedAccount!!.publicKey.take(20) + "...",
fontSize = 13.sp,
color = secondaryTextColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
// Dropdown arrow with rotation
Icon(
imageVector = Icons.Default.KeyboardArrowDown,
contentDescription = null,
tint = secondaryTextColor,
modifier = Modifier
.size(24.dp)
.graphicsLayer {
rotationZ = 180f * dropdownProgress
}
)
}
}
// Dropdown list with animation
AnimatedVisibility(
visible = isDropdownExpanded,
enter = fadeIn(tween(200)) + expandVertically(
expandFrom = Alignment.Top,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMediumLow
)
),
exit = fadeOut(tween(150)) + shrinkVertically(
shrinkTowards = Alignment.Top,
animationSpec = tween(200)
)
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
.heightIn(max = 300.dp),
colors = CardDefaults.cardColors(containerColor = cardBackground),
shape = RoundedCornerShape(16.dp)
) {
Column {
// Search field
OutlinedTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
placeholder = {
Text(
"Search accounts...",
color = secondaryTextColor.copy(alpha = 0.6f)
)
},
leadingIcon = {
Icon(
Icons.Default.Search,
contentDescription = null,
tint = secondaryTextColor
)
},
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = PrimaryBlue,
unfocusedBorderColor = Color.Transparent,
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedTextColor = textColor,
unfocusedTextColor = textColor,
cursorColor = PrimaryBlue
),
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp)
.focusRequester(searchFocusRequester),
shape = RoundedCornerShape(12.dp)
)
Divider(
color = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0),
thickness = 0.5.dp
)
// Account list
LazyColumn(
modifier = Modifier.fillMaxWidth()
) {
items(filteredAccounts, key = { it.publicKey }) { account ->
val isSelected = account.publicKey == selectedAccount?.publicKey
val itemScale by animateFloatAsState(
targetValue = if (isSelected) 1f else 0.98f,
label = "itemScale"
)
Row(
modifier = Modifier
.fillMaxWidth()
.scale(itemScale)
.clickable {
selectedAccount = account
isDropdownExpanded = false
password = ""
error = null
}
.background(
if (isSelected) PrimaryBlue.copy(alpha = 0.1f)
else Color.Transparent
)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Avatar
val avatarColors = getAvatarColor(account.name, isDarkTheme)
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(avatarColors.backgroundColor),
contentAlignment = Alignment.Center
) {
Text(
text = getAvatarText(account.publicKey),
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = avatarColors.textColor
)
}
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = account.name,
fontSize = 15.sp,
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = account.publicKey.take(16) + "...",
fontSize = 12.sp,
color = secondaryTextColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (isSelected) {
Icon(
Icons.Default.Check,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(20.dp)
)
}
}
}
if (filteredAccounts.isEmpty()) {
item {
Text(
text = "No accounts found",
color = secondaryTextColor,
fontSize = 14.sp,
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
textAlign = TextAlign.Center
)
}
}
}
}
}
}
}
}
Spacer(modifier = Modifier.height(20.dp))
// Password Field
AnimatedVisibility(
visible = visible,
visible = visible && !isDropdownExpanded,
enter = fadeIn(tween(600, delayMillis = 400)) + slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 400)
@@ -197,7 +500,7 @@ fun UnlockScreen(
// Unlock Button
AnimatedVisibility(
visible = visible,
visible = visible && !isDropdownExpanded,
enter = fadeIn(tween(600, delayMillis = 500)) + slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 500)
@@ -205,6 +508,10 @@ fun UnlockScreen(
) {
Button(
onClick = {
if (selectedAccount == null) {
error = "Please select an account"
return@Button
}
if (password.isEmpty()) {
error = "Please enter your password"
return@Button
@@ -213,20 +520,7 @@ fun UnlockScreen(
isUnlocking = true
scope.launch {
try {
val publicKey = currentPublicKey ?: run {
error = "No account found"
isUnlocking = false
return@launch
}
val accounts = accountManager.getAllAccounts()
val account = accounts.find { it.publicKey == publicKey }
if (account == null) {
error = "Account not found"
isUnlocking = false
return@launch
}
val account = selectedAccount!!.encryptedAccount
// Try to decrypt
val decryptedPrivateKey = CryptoManager.decryptWithPassword(
@@ -253,20 +547,20 @@ fun UnlockScreen(
name = account.name
)
// 🔌 Connect to server and authenticate
Log.d("UnlockScreen", "🔌 Connecting to server...")
// Connect to server and authenticate
Log.d("UnlockScreen", "Connecting to server...")
ProtocolManager.authenticate(account.publicKey, privateKeyHash)
accountManager.setCurrentAccount(publicKey)
accountManager.setCurrentAccount(account.publicKey)
onUnlocked(decryptedAccount)
} catch (e: Exception) {
error = "Failed to unlock: ${e.message}"
error = "Failed to unlock: \${e.message}"
isUnlocking = false
}
}
},
enabled = password.isNotEmpty() && !isUnlocking,
enabled = selectedAccount != null && password.isNotEmpty() && !isUnlocking,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
@@ -302,9 +596,9 @@ fun UnlockScreen(
Spacer(modifier = Modifier.height(16.dp))
// Switch Account button
// Create New Account button
AnimatedVisibility(
visible = visible,
visible = visible && !isDropdownExpanded,
enter = fadeIn(tween(600, delayMillis = 600)) + slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 600)
@@ -314,21 +608,21 @@ fun UnlockScreen(
onClick = onSwitchAccount
) {
Icon(
imageVector = Icons.Default.SwapHoriz,
imageVector = Icons.Default.PersonAdd,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Switch Account",
text = "Create New Account",
color = PrimaryBlue,
fontSize = 15.sp
)
}
}
Spacer(modifier = Modifier.weight(0.3f))
Spacer(modifier = Modifier.height(60.dp))
}
}
}

View File

@@ -148,73 +148,30 @@ fun ChatsListScreen(
onNewChat: () -> Unit,
onLogout: () -> Unit
) {
// Theme transition animation state
var isTransitioning by remember { mutableStateOf(false) }
var transitionProgress by remember { mutableStateOf(0f) }
var clickPosition by remember { mutableStateOf(Offset.Zero) }
var shouldUpdateStatusBar by remember { mutableStateOf(false) }
// Theme transition state
var hasInitialized by remember { mutableStateOf(false) }
var previousTheme by remember { mutableStateOf(isDarkTheme) }
var targetTheme by remember { mutableStateOf(isDarkTheme) }
LaunchedEffect(Unit) {
hasInitialized = true
}
// Theme transition animation
LaunchedEffect(isTransitioning) {
if (isTransitioning) {
shouldUpdateStatusBar = false
val duration = 800f
val startTime = System.currentTimeMillis()
while (transitionProgress < 1f) {
val elapsed = System.currentTimeMillis() - startTime
transitionProgress = (elapsed / duration).coerceAtMost(1f)
kotlinx.coroutines.delay(16)
}
shouldUpdateStatusBar = true
kotlinx.coroutines.delay(50)
isTransitioning = false
transitionProgress = 0f
shouldUpdateStatusBar = false
previousTheme = targetTheme
}
}
val view = androidx.compose.ui.platform.LocalView.current
// Animate navigation bar color starting at 80% of wave animation
LaunchedEffect(isTransitioning, transitionProgress) {
if (isTransitioning && transitionProgress >= 0.8f && !view.isInEditMode) {
val window = (view.context as android.app.Activity).window
val navProgress = ((transitionProgress - 0.8f) / 0.2f).coerceIn(0f, 1f)
val oldColor = if (previousTheme) 0xFF1A1A1A else 0xFFFFFFFF
val newColor = if (targetTheme) 0xFF1A1A1A else 0xFFFFFFFF
val r1 = (oldColor shr 16 and 0xFF)
val g1 = (oldColor shr 8 and 0xFF)
val b1 = (oldColor and 0xFF)
val r2 = (newColor shr 16 and 0xFF)
val g2 = (newColor shr 8 and 0xFF)
val b2 = (newColor and 0xFF)
val r = (r1 + (r2 - r1) * navProgress).toInt()
val g = (g1 + (g2 - g1) * navProgress).toInt()
val b = (b1 + (b2 - b1) * navProgress).toInt()
window.navigationBarColor = (0xFF000000 or (r.toLong() shl 16) or (g.toLong() shl 8) or b.toLong()).toInt()
}
}
// Update status bar icons when animation finishes
LaunchedEffect(shouldUpdateStatusBar) {
if (shouldUpdateStatusBar && !view.isInEditMode) {
// Update status bar and navigation bar
LaunchedEffect(isDarkTheme, drawerState.isOpen) {
if (!view.isInEditMode) {
val window = (view.context as android.app.Activity).window
val insetsController = androidx.core.view.WindowCompat.getInsetsController(window, view)
insetsController.isAppearanceLightStatusBars = !isDarkTheme
insetsController.isAppearanceLightNavigationBars = !isDarkTheme
window.statusBarColor = android.graphics.Color.TRANSPARENT
// When drawer is open, dim the navigation bar to match overlay
window.navigationBarColor = if (drawerState.isOpen) {
// Darker color to match scrim overlay
if (isDarkTheme) 0xFF0D0D0D.toInt() else 0xFF999999.toInt()
} else {
if (isDarkTheme) 0xFF1A1A1A.toInt() else 0xFFFFFFFF.toInt()
}
}
}
@@ -371,29 +328,12 @@ fun ChatsListScreen(
)
Box(modifier = Modifier.fillMaxSize()) {
// Base background - shows the OLD theme color during transition
// Simple background
Box(
modifier = Modifier
.fillMaxSize()
.background(if (isTransitioning) {
if (previousTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
} else backgroundColor)
.background(backgroundColor)
)
// Circular reveal overlay - draws the NEW theme color expanding
if (isTransitioning) {
Canvas(modifier = Modifier.fillMaxSize()) {
val maxRadius = kotlin.math.hypot(size.width, size.height)
val radius = maxRadius * transitionProgress
// Draw the NEW theme color expanding from click point
drawCircle(
color = if (targetTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF),
radius = radius,
center = clickPosition
)
}
}
ModalNavigationDrawer(
drawerState = drawerState,
@@ -406,8 +346,9 @@ fun ChatsListScreen(
Box(
modifier = Modifier
.fillMaxWidth()
.background(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5))
.padding(16.dp)
.background(drawerBackgroundColor)
.padding(top = 48.dp)
.padding(horizontal = 16.dp, vertical = 16.dp)
) {
Column {
Row(
@@ -434,46 +375,25 @@ fun ChatsListScreen(
// Theme toggle
IconButton(
onClick = {},
modifier = Modifier.onGloballyPositioned { coordinates ->
// This will be handled by clickable below
}
onClick = onToggleTheme
) {
Box(
modifier = Modifier
.clickable {
if (!isTransitioning) {
previousTheme = isDarkTheme
targetTheme = !isDarkTheme
// Use center of icon as click position
val screenWidth = view.width.toFloat()
val screenHeight = view.height.toFloat()
clickPosition = Offset(
screenWidth - 48.dp.value * view.resources.displayMetrics.density,
96.dp.value * view.resources.displayMetrics.density
)
isTransitioning = true
onToggleTheme()
}
}
) {
Icon(
if (isDarkTheme) Icons.Default.LightMode else Icons.Default.DarkMode,
contentDescription = "Toggle theme",
tint = textColor
)
}
Icon(
if (isDarkTheme) Icons.Default.LightMode else Icons.Default.DarkMode,
contentDescription = "Toggle theme",
tint = textColor
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Account name
// Public key instead of account name
Text(
text = accountName,
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
color = textColor
text = accountPublicKey,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = secondaryTextColor,
maxLines = 1
)
}
}
@@ -508,6 +428,8 @@ fun ChatsListScreen(
}
}
Spacer(modifier = Modifier.weight(1f))
// Logout button at bottom
Divider(
modifier = Modifier.padding(horizontal = 16.dp),
@@ -545,6 +467,7 @@ fun ChatsListScreen(
)
}
Spacer(modifier = Modifier.navigationBarsPadding())
Spacer(modifier = Modifier.height(16.dp))
}
}