feat: Implement collapsing header for ProfileScreen with enhanced navigation and logging features
This commit is contained in:
@@ -4,8 +4,10 @@ import android.content.ClipData
|
|||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
|
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.LazyColumn
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
@@ -16,22 +18,28 @@ import androidx.compose.material3.*
|
|||||||
import androidx.compose.runtime.*
|
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.alpha
|
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
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.Dp
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
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 androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
// 🎨 Avatar colors - используем те же цвета что и в ChatsListScreen
|
// 🎨 Avatar colors - используем те же цвета что и в ChatsListScreen
|
||||||
private val avatarColorsLight =
|
private val avatarColorsLight =
|
||||||
@@ -91,6 +99,15 @@ private fun getInitials(name: String): String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
// 🎯 COLLAPSING HEADER CONSTANTS
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
private val EXPANDED_HEADER_HEIGHT = 280.dp
|
||||||
|
private val COLLAPSED_HEADER_HEIGHT = 56.dp
|
||||||
|
private val AVATAR_SIZE_EXPANDED = 120.dp
|
||||||
|
private val AVATAR_SIZE_COLLAPSED = 36.dp
|
||||||
|
private val STATUS_BAR_HEIGHT = 24.dp
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileScreen(
|
fun ProfileScreen(
|
||||||
@@ -107,13 +124,12 @@ fun ProfileScreen(
|
|||||||
onNavigateToLogs: () -> Unit = {},
|
onNavigateToLogs: () -> Unit = {},
|
||||||
viewModel: ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
|
viewModel: ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
|
||||||
) {
|
) {
|
||||||
// Цвета в зависимости от темы - такие же как в ChatsListScreen
|
// Цвета в зависимости от темы
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
|
||||||
val surfaceColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
|
val surfaceColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
|
||||||
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 dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8)
|
val avatarColors = getAvatarColor(accountPublicKey, isDarkTheme)
|
||||||
val iconTintColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
|
||||||
|
|
||||||
// State for editing
|
// State for editing
|
||||||
var editedName by remember { mutableStateOf(accountName) }
|
var editedName by remember { mutableStateOf(accountName) }
|
||||||
@@ -123,8 +139,50 @@ fun ProfileScreen(
|
|||||||
// ViewModel state
|
// ViewModel state
|
||||||
val profileState by viewModel.state.collectAsState()
|
val profileState by viewModel.state.collectAsState()
|
||||||
|
|
||||||
// Show success toast
|
// Scroll state for collapsing header animation
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val expandedHeightPx = with(density) { EXPANDED_HEADER_HEIGHT.toPx() }
|
||||||
|
val collapsedHeightPx = with(density) { (COLLAPSED_HEADER_HEIGHT + STATUS_BAR_HEIGHT).toPx() }
|
||||||
|
|
||||||
|
// Track scroll offset with animated state for smooth transitions
|
||||||
|
var scrollOffset by remember { mutableFloatStateOf(0f) }
|
||||||
|
val maxScrollOffset = expandedHeightPx - collapsedHeightPx
|
||||||
|
|
||||||
|
// Calculate collapse progress (0 = expanded, 1 = collapsed)
|
||||||
|
val collapseProgress by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
(scrollOffset / maxScrollOffset).coerceIn(0f, 1f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nested scroll connection for tracking scroll
|
||||||
|
val nestedScrollConnection = remember {
|
||||||
|
object : NestedScrollConnection {
|
||||||
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
val delta = available.y
|
||||||
|
val newOffset = scrollOffset - delta
|
||||||
|
val consumed = when {
|
||||||
|
delta < 0 && scrollOffset < maxScrollOffset -> {
|
||||||
|
val consumed = (newOffset.coerceIn(0f, maxScrollOffset) - scrollOffset)
|
||||||
|
scrollOffset = newOffset.coerceIn(0f, maxScrollOffset)
|
||||||
|
-consumed
|
||||||
|
}
|
||||||
|
delta > 0 && scrollOffset > 0 -> {
|
||||||
|
val consumed = scrollOffset - newOffset.coerceIn(0f, maxScrollOffset)
|
||||||
|
scrollOffset = newOffset.coerceIn(0f, maxScrollOffset)
|
||||||
|
consumed
|
||||||
|
}
|
||||||
|
else -> 0f
|
||||||
|
}
|
||||||
|
return Offset(0f, consumed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
// Show success toast
|
||||||
LaunchedEffect(profileState.saveSuccess) {
|
LaunchedEffect(profileState.saveSuccess) {
|
||||||
if (profileState.saveSuccess) {
|
if (profileState.saveSuccess) {
|
||||||
android.widget.Toast.makeText(
|
android.widget.Toast.makeText(
|
||||||
@@ -159,198 +217,365 @@ fun ProfileScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
|
.nestedScroll(nestedScrollConnection)
|
||||||
) {
|
) {
|
||||||
Column(
|
// Scrollable content
|
||||||
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.padding(top = with(density) { (expandedHeightPx - scrollOffset).toDp() })
|
||||||
) {
|
) {
|
||||||
// ═════════════════════════════════════════════════════════════
|
item {
|
||||||
// 👤 PROFILE CARD with colored header
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
// ═════════════════════════════════════════════════════════════
|
|
||||||
ProfileCard(
|
// ═════════════════════════════════════════════════════════════
|
||||||
name = editedName.ifBlank { accountPublicKey.take(10) },
|
// ✏️ EDITABLE FIELDS
|
||||||
username = editedUsername,
|
// ═════════════════════════════════════════════════════════════
|
||||||
publicKey = accountPublicKey,
|
ProfileSectionTitle(title = "Profile Information", isDarkTheme = isDarkTheme)
|
||||||
isDarkTheme = isDarkTheme,
|
|
||||||
onBack = null, // Кнопка назад будет снаружи
|
Surface(
|
||||||
hasChanges = hasChanges,
|
modifier = Modifier
|
||||||
onSave = {
|
.fillMaxWidth()
|
||||||
// Save via ViewModel
|
.padding(horizontal = 16.dp),
|
||||||
viewModel.saveProfile(
|
color = surfaceColor,
|
||||||
publicKey = accountPublicKey,
|
shape = RoundedCornerShape(16.dp)
|
||||||
privateKeyHash = accountPrivateKeyHash,
|
) {
|
||||||
name = editedName,
|
Column {
|
||||||
username = editedUsername
|
ProfileEditableField(
|
||||||
)
|
label = "Your name",
|
||||||
// Also update local account name
|
value = editedName,
|
||||||
viewModel.updateLocalAccountName(accountPublicKey, editedName)
|
onValueChange = { editedName = it },
|
||||||
|
placeholder = "ex. Freddie Gibson",
|
||||||
|
isDarkTheme = isDarkTheme,
|
||||||
|
showDivider = true
|
||||||
|
)
|
||||||
|
ProfileEditableField(
|
||||||
|
label = "Username",
|
||||||
|
value = editedUsername,
|
||||||
|
onValueChange = { editedUsername = it },
|
||||||
|
placeholder = "ex. freddie871",
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════
|
// Public Key Copy Field
|
||||||
// ✏️ EDITABLE FIELDS
|
ProfileCopyField(
|
||||||
// ═════════════════════════════════════════════════════════════
|
label = "Public Key",
|
||||||
ProfileSectionTitle(title = "Profile Information", isDarkTheme = isDarkTheme)
|
value = accountPublicKey,
|
||||||
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
color = surfaceColor,
|
|
||||||
shape = RoundedCornerShape(16.dp)
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
ProfileEditableField(
|
|
||||||
label = "Your name",
|
|
||||||
value = editedName,
|
|
||||||
onValueChange = { editedName = it },
|
|
||||||
placeholder = "ex. Freddie Gibson",
|
|
||||||
isDarkTheme = isDarkTheme,
|
|
||||||
showDivider = true
|
|
||||||
)
|
|
||||||
ProfileEditableField(
|
|
||||||
label = "Username",
|
|
||||||
value = editedUsername,
|
|
||||||
onValueChange = { editedUsername = it },
|
|
||||||
placeholder = "ex. freddie871",
|
|
||||||
isDarkTheme = isDarkTheme
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
// Public Key Copy Field
|
|
||||||
ProfileCopyField(
|
|
||||||
label = "Public Key",
|
|
||||||
value = accountPublicKey,
|
|
||||||
isDarkTheme = isDarkTheme
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "This is your public key. If you haven't set a @username yet, you can ask a friend to message you using your public key.",
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = secondaryTextColor,
|
|
||||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
|
||||||
lineHeight = 16.sp
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════
|
|
||||||
// 🔧 SETTINGS SECTION
|
|
||||||
// ═════════════════════════════════════════════════════════════
|
|
||||||
ProfileSectionTitle(title = "Settings", isDarkTheme = isDarkTheme)
|
|
||||||
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
color = surfaceColor,
|
|
||||||
shape = RoundedCornerShape(16.dp)
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
ProfileNavigationItem(
|
|
||||||
icon = Icons.Outlined.Brush,
|
|
||||||
iconBackground = Color(0xFF6366F1), // indigo
|
|
||||||
title = "Theme",
|
|
||||||
subtitle = "Customize appearance",
|
|
||||||
onClick = onNavigateToTheme,
|
|
||||||
isDarkTheme = isDarkTheme,
|
|
||||||
showDivider = true
|
|
||||||
)
|
|
||||||
ProfileNavigationItem(
|
|
||||||
icon = Icons.Outlined.AdminPanelSettings,
|
|
||||||
iconBackground = Color(0xFF9333EA), // grape/purple
|
|
||||||
title = "Safety",
|
|
||||||
subtitle = "Backup and security settings",
|
|
||||||
onClick = onNavigateToSafety,
|
|
||||||
isDarkTheme = isDarkTheme
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "You can learn more about your safety on the safety page, please make sure you are viewing the screen alone before proceeding to the safety page.",
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = secondaryTextColor,
|
|
||||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
|
||||||
lineHeight = 16.sp
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════
|
|
||||||
// <20> DEBUG / LOGS SECTION
|
|
||||||
// ═════════════════════════════════════════════════════════════
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
color = surfaceColor,
|
|
||||||
shape = RoundedCornerShape(16.dp)
|
|
||||||
) {
|
|
||||||
ProfileNavigationItem(
|
|
||||||
icon = Icons.Outlined.BugReport,
|
|
||||||
iconBackground = Color(0xFFFB8C00), // orange
|
|
||||||
title = "View Logs",
|
|
||||||
subtitle = "Debug profile save operations",
|
|
||||||
onClick = onNavigateToLogs,
|
|
||||||
isDarkTheme = isDarkTheme
|
isDarkTheme = isDarkTheme
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "View detailed logs of profile save operations for debugging.",
|
text = "This is your public key. If you haven't set a @username yet, you can ask a friend to message you using your public key.",
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = secondaryTextColor,
|
color = secondaryTextColor,
|
||||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||||
lineHeight = 16.sp
|
lineHeight = 16.sp
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════
|
|
||||||
// <20>🚪 LOGOUT SECTION
|
|
||||||
// ═════════════════════════════════════════════════════════════
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
color = surfaceColor,
|
|
||||||
shape = RoundedCornerShape(16.dp)
|
|
||||||
) {
|
|
||||||
ProfileNavigationItem(
|
|
||||||
icon = Icons.Outlined.Logout,
|
|
||||||
iconBackground = if (isDarkTheme) Color(0xFFFF8787) else Color(0xFFEF4444),
|
|
||||||
title = "Logout",
|
|
||||||
subtitle = "Sign out of your account",
|
|
||||||
onClick = onLogout,
|
|
||||||
isDarkTheme = isDarkTheme,
|
|
||||||
hideChevron = true,
|
|
||||||
textColor = if (isDarkTheme) Color(0xFFFF8787) else Color(0xFFEF4444)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
// 🔧 SETTINGS SECTION
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
ProfileSectionTitle(title = "Settings", isDarkTheme = isDarkTheme)
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
color = surfaceColor,
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
ProfileNavigationItem(
|
||||||
|
icon = Icons.Outlined.Brush,
|
||||||
|
iconBackground = Color(0xFF6366F1),
|
||||||
|
title = "Theme",
|
||||||
|
subtitle = "Customize appearance",
|
||||||
|
onClick = onNavigateToTheme,
|
||||||
|
isDarkTheme = isDarkTheme,
|
||||||
|
showDivider = true
|
||||||
|
)
|
||||||
|
ProfileNavigationItem(
|
||||||
|
icon = Icons.Outlined.AdminPanelSettings,
|
||||||
|
iconBackground = Color(0xFF9333EA),
|
||||||
|
title = "Safety",
|
||||||
|
subtitle = "Backup and security settings",
|
||||||
|
onClick = onNavigateToSafety,
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "You can learn more about your safety on the safety page, please make sure you are viewing the screen alone before proceeding to the safety page.",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = secondaryTextColor,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||||
|
lineHeight = 16.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
// 🐛 DEBUG / LOGS SECTION
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
color = surfaceColor,
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
) {
|
||||||
|
ProfileNavigationItem(
|
||||||
|
icon = Icons.Outlined.BugReport,
|
||||||
|
iconBackground = Color(0xFFFB8C00),
|
||||||
|
title = "View Logs",
|
||||||
|
subtitle = "Debug profile save operations",
|
||||||
|
onClick = onNavigateToLogs,
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "View detailed logs of profile save operations for debugging.",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = secondaryTextColor,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||||
|
lineHeight = 16.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
// 🚪 LOGOUT SECTION
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
color = surfaceColor,
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
) {
|
||||||
|
ProfileNavigationItem(
|
||||||
|
icon = Icons.Outlined.Logout,
|
||||||
|
iconBackground = if (isDarkTheme) Color(0xFFFF8787) else Color(0xFFEF4444),
|
||||||
|
title = "Logout",
|
||||||
|
subtitle = "Sign out of your account",
|
||||||
|
onClick = onLogout,
|
||||||
|
isDarkTheme = isDarkTheme,
|
||||||
|
hideChevron = true,
|
||||||
|
textColor = if (isDarkTheme) Color(0xFFFF8787) else Color(0xFFEF4444)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Logging out of your account. After logging out, you will be redirected to the password entry page.",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = secondaryTextColor,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||||
|
lineHeight = 16.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "Logging out of your account. After logging out, you will be redirected to the password entry page.",
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = secondaryTextColor,
|
|
||||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
|
||||||
lineHeight = 16.sp
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fixed back button at top
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
// 🎨 COLLAPSING HEADER - Telegram style
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
CollapsingProfileHeader(
|
||||||
|
name = editedName.ifBlank { accountPublicKey.take(10) },
|
||||||
|
username = editedUsername,
|
||||||
|
publicKey = accountPublicKey,
|
||||||
|
avatarColors = avatarColors,
|
||||||
|
collapseProgress = collapseProgress,
|
||||||
|
onBack = onBack,
|
||||||
|
hasChanges = hasChanges,
|
||||||
|
onSave = {
|
||||||
|
viewModel.saveProfile(
|
||||||
|
publicKey = accountPublicKey,
|
||||||
|
privateKeyHash = accountPrivateKeyHash,
|
||||||
|
name = editedName,
|
||||||
|
username = editedUsername
|
||||||
|
)
|
||||||
|
viewModel.updateLocalAccountName(accountPublicKey, editedName)
|
||||||
|
},
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
// 🎯 COLLAPSING PROFILE HEADER - Telegram Style Animation
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
@Composable
|
||||||
|
private fun CollapsingProfileHeader(
|
||||||
|
name: String,
|
||||||
|
username: String,
|
||||||
|
publicKey: String,
|
||||||
|
avatarColors: AvatarColors,
|
||||||
|
collapseProgress: Float,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
hasChanges: Boolean,
|
||||||
|
onSave: () -> Unit,
|
||||||
|
isDarkTheme: Boolean
|
||||||
|
) {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
// Animated values using lerp
|
||||||
|
val headerHeight = androidx.compose.ui.unit.lerp(
|
||||||
|
EXPANDED_HEADER_HEIGHT,
|
||||||
|
COLLAPSED_HEADER_HEIGHT + STATUS_BAR_HEIGHT,
|
||||||
|
collapseProgress
|
||||||
|
)
|
||||||
|
|
||||||
|
val avatarSize = androidx.compose.ui.unit.lerp(
|
||||||
|
AVATAR_SIZE_EXPANDED,
|
||||||
|
AVATAR_SIZE_COLLAPSED,
|
||||||
|
collapseProgress
|
||||||
|
)
|
||||||
|
|
||||||
|
// Avatar position - smooth transition from center to left
|
||||||
|
val screenWidthDp = with(density) { 360.dp } // approximate
|
||||||
|
val avatarStartX = (screenWidthDp - AVATAR_SIZE_EXPANDED) / 2
|
||||||
|
val avatarEndX = 56.dp // After back button
|
||||||
|
|
||||||
|
val avatarX = androidx.compose.ui.unit.lerp(avatarStartX, avatarEndX, collapseProgress)
|
||||||
|
val avatarY = androidx.compose.ui.unit.lerp(70.dp, STATUS_BAR_HEIGHT + 10.dp, collapseProgress)
|
||||||
|
|
||||||
|
// Text alpha animations
|
||||||
|
val expandedContentAlpha = (1f - collapseProgress * 2f).coerceIn(0f, 1f) // Fades out faster
|
||||||
|
val collapsedContentAlpha = ((collapseProgress - 0.5f) * 2f).coerceIn(0f, 1f) // Fades in at 50%
|
||||||
|
|
||||||
|
// Avatar scale for "drop" effect - shrinks faster at the end
|
||||||
|
val avatarScaleMultiplier = if (collapseProgress > 0.7f) {
|
||||||
|
1f - ((collapseProgress - 0.7f) / 0.3f) * 0.15f
|
||||||
|
} else 1f
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(headerHeight)
|
||||||
|
.drawBehind {
|
||||||
|
drawRect(avatarColors.backgroundColor)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 👤 ANIMATED AVATAR - Telegram "drop" effect
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.offset(x = avatarX, y = avatarY)
|
||||||
|
.size(avatarSize)
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = avatarScaleMultiplier
|
||||||
|
scaleY = avatarScaleMultiplier
|
||||||
|
// Subtle alpha decrease at the very end for smooth collapse
|
||||||
|
alpha = if (collapseProgress > 0.9f) {
|
||||||
|
1f - ((collapseProgress - 0.9f) / 0.1f) * 0.2f
|
||||||
|
} else 1f
|
||||||
|
}
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color.White.copy(alpha = 0.2f))
|
||||||
|
.padding(2.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(avatarColors.backgroundColor),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = getInitials(name),
|
||||||
|
fontSize = androidx.compose.ui.unit.lerp(40.sp, 14.sp, collapseProgress),
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = avatarColors.textColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 📝 EXPANDED STATE - Name and info below avatar
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 200.dp)
|
||||||
|
.graphicsLayer { alpha = expandedContentAlpha },
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
// Name
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = Color.White,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
// Username and public key
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (username.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = "@$username",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color.White.copy(alpha = 0.8f)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = " • ",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color.White.copy(alpha = 0.8f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = "${publicKey.take(3)}...${publicKey.takeLast(3)}",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color.White.copy(alpha = 0.8f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 📝 COLLAPSED STATE - Toolbar with name next to avatar
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.statusBarsPadding()
|
||||||
|
.height(COLLAPSED_HEADER_HEIGHT)
|
||||||
|
.padding(start = 100.dp) // Space for back button + avatar
|
||||||
|
.graphicsLayer { alpha = collapsedContentAlpha },
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = Color.White,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "online",
|
||||||
|
fontSize = 13.sp,
|
||||||
|
color = Color.White.copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🔙 BACK BUTTON - Always visible
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onBack,
|
onClick = onBack,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopStart)
|
|
||||||
.statusBarsPadding()
|
.statusBarsPadding()
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
) {
|
) {
|
||||||
@@ -361,7 +586,9 @@ fun ProfileScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fixed save button (if changes)
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 💾 SAVE BUTTON (if changes)
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = hasChanges,
|
visible = hasChanges,
|
||||||
enter = fadeIn() + expandHorizontally(),
|
enter = fadeIn() + expandHorizontally(),
|
||||||
@@ -371,10 +598,7 @@ fun ProfileScreen(
|
|||||||
.statusBarsPadding()
|
.statusBarsPadding()
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = onSave) {
|
||||||
onSaveProfile(editedName, editedUsername)
|
|
||||||
hasChanges = false
|
|
||||||
}) {
|
|
||||||
Text(
|
Text(
|
||||||
text = "Save",
|
text = "Save",
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
@@ -386,7 +610,7 @@ fun ProfileScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════
|
||||||
// 📦 PROFILE CARD COMPONENT - Large Avatar Telegram Style
|
// 📦 PROFILE CARD COMPONENT - Legacy (kept for OtherProfileScreen)
|
||||||
// ═════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileCard(
|
fun ProfileCard(
|
||||||
@@ -649,7 +873,7 @@ private fun ProfileCopyField(
|
|||||||
Text(
|
Text(
|
||||||
text = "Copied!",
|
text = "Copied!",
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = Color(0xFF22C55E), // green
|
color = Color(0xFF22C55E),
|
||||||
fontWeight = FontWeight.Medium
|
fontWeight = FontWeight.Medium
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user