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,35 +217,15 @@ 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))
// ═════════════════════════════════════════════════════════════
@@ -255,7 +293,7 @@ fun ProfileScreen(
Column {
ProfileNavigationItem(
icon = Icons.Outlined.Brush,
iconBackground = Color(0xFF6366F1), // indigo
iconBackground = Color(0xFF6366F1),
title = "Theme",
subtitle = "Customize appearance",
onClick = onNavigateToTheme,
@@ -264,7 +302,7 @@ fun ProfileScreen(
)
ProfileNavigationItem(
icon = Icons.Outlined.AdminPanelSettings,
iconBackground = Color(0xFF9333EA), // grape/purple
iconBackground = Color(0xFF9333EA),
title = "Safety",
subtitle = "Backup and security settings",
onClick = onNavigateToSafety,
@@ -284,7 +322,7 @@ fun ProfileScreen(
Spacer(modifier = Modifier.height(24.dp))
// ═════════════════════════════════════════════════════════════
// <EFBFBD> DEBUG / LOGS SECTION
// 🐛 DEBUG / LOGS SECTION
// ═════════════════════════════════════════════════════════════
Surface(
modifier = Modifier
@@ -295,7 +333,7 @@ fun ProfileScreen(
) {
ProfileNavigationItem(
icon = Icons.Outlined.BugReport,
iconBackground = Color(0xFFFB8C00), // orange
iconBackground = Color(0xFFFB8C00),
title = "View Logs",
subtitle = "Debug profile save operations",
onClick = onNavigateToLogs,
@@ -314,7 +352,7 @@ fun ProfileScreen(
Spacer(modifier = Modifier.height(24.dp))
// ═════════════════════════════════════════════════════════════
// <EFBFBD>🚪 LOGOUT SECTION
// 🚪 LOGOUT SECTION
// ═════════════════════════════════════════════════════════════
Surface(
modifier = Modifier
@@ -345,12 +383,199 @@ fun ProfileScreen(
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 {