feat: Implement collapsing header for ProfileScreen with enhanced navigation and logging features

This commit is contained in:
k1ngsterr1
2026-01-21 00:08:33 +05:00
parent db61ebb79f
commit f856459494

View File

@@ -4,8 +4,10 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
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.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
@@ -16,22 +18,28 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
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.graphicsLayer
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.LocalDensity
import androidx.compose.ui.text.TextStyle
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.IntOffset
import androidx.compose.ui.unit.dp
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.launch
import kotlin.math.roundToInt
// 🎨 Avatar colors - используем те же цвета что и в ChatsListScreen
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)
@Composable
fun ProfileScreen(
@@ -107,13 +124,12 @@ fun ProfileScreen(
onNavigateToLogs: () -> Unit = {},
viewModel: ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
) {
// Цвета в зависимости от темы - такие же как в ChatsListScreen
// Цвета в зависимости от темы
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
val surfaceColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8)
val iconTintColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val avatarColors = getAvatarColor(accountPublicKey, isDarkTheme)
// State for editing
var editedName by remember { mutableStateOf(accountName) }
@@ -123,8 +139,50 @@ fun ProfileScreen(
// ViewModel state
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
// Show success toast
LaunchedEffect(profileState.saveSuccess) {
if (profileState.saveSuccess) {
android.widget.Toast.makeText(
@@ -159,198 +217,365 @@ fun ProfileScreen(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
.nestedScroll(nestedScrollConnection)
) {
Column(
// Scrollable content
LazyColumn(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(top = with(density) { (expandedHeightPx - scrollOffset).toDp() })
) {
// ═════════════════════════════════════════════════════════════
// 👤 PROFILE CARD with colored header
// ═════════════════════════════════════════════════════════════
ProfileCard(
name = editedName.ifBlank { accountPublicKey.take(10) },
username = editedUsername,
publicKey = accountPublicKey,
isDarkTheme = isDarkTheme,
onBack = null, // Кнопка назад будет снаружи
hasChanges = hasChanges,
onSave = {
// Save via ViewModel
viewModel.saveProfile(
publicKey = accountPublicKey,
privateKeyHash = accountPrivateKeyHash,
name = editedName,
username = editedUsername
)
// Also update local account name
viewModel.updateLocalAccountName(accountPublicKey, editedName)
item {
Spacer(modifier = Modifier.height(16.dp))
// ═════════════════════════════════════════════════════════════
// ✏️ EDITABLE FIELDS
// ═════════════════════════════════════════════════════════════
ProfileSectionTitle(title = "Profile Information", isDarkTheme = isDarkTheme)
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(16.dp))
Spacer(modifier = Modifier.height(8.dp))
// ═════════════════════════════════════════════════════════════
// ✏️ EDITABLE FIELDS
// ═════════════════════════════════════════════════════════════
ProfileSectionTitle(title = "Profile Information", isDarkTheme = isDarkTheme)
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,
// Public Key Copy Field
ProfileCopyField(
label = "Public Key",
value = accountPublicKey,
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))
// ═════════════════════════════════════════════════════════════
// <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)
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),
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(
onClick = onBack,
modifier = Modifier
.align(Alignment.TopStart)
.statusBarsPadding()
.padding(4.dp)
) {
@@ -361,7 +586,9 @@ fun ProfileScreen(
)
}
// Fixed save button (if changes)
// ═══════════════════════════════════════════════════════════
// 💾 SAVE BUTTON (if changes)
// ═══════════════════════════════════════════════════════════
AnimatedVisibility(
visible = hasChanges,
enter = fadeIn() + expandHorizontally(),
@@ -371,10 +598,7 @@ fun ProfileScreen(
.statusBarsPadding()
.padding(4.dp)
) {
TextButton(onClick = {
onSaveProfile(editedName, editedUsername)
hasChanges = false
}) {
TextButton(onClick = onSave) {
Text(
text = "Save",
color = Color.White,
@@ -386,7 +610,7 @@ fun ProfileScreen(
}
// ═════════════════════════════════════════════════════════════
// 📦 PROFILE CARD COMPONENT - Large Avatar Telegram Style
// 📦 PROFILE CARD COMPONENT - Legacy (kept for OtherProfileScreen)
// ═════════════════════════════════════════════════════════════
@Composable
fun ProfileCard(
@@ -649,7 +873,7 @@ private fun ProfileCopyField(
Text(
text = "Copied!",
fontSize = 14.sp,
color = Color(0xFF22C55E), // green
color = Color(0xFF22C55E),
fontWeight = FontWeight.Medium
)
} else {