Implement feature X to enhance user experience and optimize performance

This commit is contained in:
k1ngsterr1
2026-01-31 03:14:37 +05:00
parent 5f87f091f7
commit d9453edd05

View File

@@ -291,203 +291,119 @@ fun ProfileScreen(
// ViewModel state // ViewModel state
val profileState by viewModel.state.collectAsState() val profileState by viewModel.state.collectAsState()
// Scroll state for collapsing header animation // Scroll state for collapsing header + overscroll avatar expansion
val density = LocalDensity.current val density = LocalDensity.current
val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
// Header heights
val expandedHeightPx = with(density) { (EXPANDED_HEADER_HEIGHT + statusBarHeight).toPx() } val expandedHeightPx = with(density) { (EXPANDED_HEADER_HEIGHT + statusBarHeight).toPx() }
val collapsedHeightPx = with(density) { (COLLAPSED_HEADER_HEIGHT + statusBarHeight).toPx() } val collapsedHeightPx = with(density) { (COLLAPSED_HEADER_HEIGHT + statusBarHeight).toPx() }
// Track scroll offset with animated state for smooth transitions
// Может быть отрицательным для overscroll (оттягивание вверх)
var scrollOffset by remember { mutableFloatStateOf(0f) }
val maxScrollOffset = expandedHeightPx - collapsedHeightPx val maxScrollOffset = expandedHeightPx - collapsedHeightPx
val minScrollOffset = -expandedHeightPx * 0.5f // Максимальный overscroll (50% высоты)
// Порог для snap - если overscroll > 50%, то snap к квадрату, иначе к кругу // Track scroll offset for collapsing (скролл вверх = collapse)
val snapThreshold = minScrollOffset * 0.5f var scrollOffset by remember { mutableFloatStateOf(0f) }
// Анимированный overscroll offset для плавного snap // Calculate collapse progress (0 = expanded, 1 = collapsed)
val animatedScrollOffset = remember { androidx.compose.animation.core.Animatable(0f) } val collapseProgress by remember {
derivedStateOf { (scrollOffset / maxScrollOffset).coerceIn(0f, 1f) }
}
// Отслеживаем активный drag // Dynamic header height based on scroll
val headerHeight =
with(density) {
(expandedHeightPx - scrollOffset).coerceAtLeast(collapsedHeightPx).toDp()
}
// Track overscroll offset for avatar expansion (скролл вниз при достижении верха)
var overscrollOffset by remember { mutableFloatStateOf(0f) }
val maxOverscroll = with(density) { 200.dp.toPx() }
val snapThreshold = maxOverscroll * 0.5f
// Track if user is currently dragging
var isDragging by remember { mutableStateOf(false) } var isDragging by remember { mutableStateOf(false) }
// Отслеживаем направление последнего скролла: true = вверх кругу), false = вниз (к квадрату) // Целевое значение для snap: либо 0 (круг), либо maxOverscroll (квадрат)
var lastScrollDirectionUp by remember { mutableStateOf(false) } val snapTarget by remember {
// Calculate collapse progress (0 = expanded, 1 = collapsed, negative = overscroll)
val collapseProgress by remember {
derivedStateOf { (animatedScrollOffset.value / maxScrollOffset).coerceIn(-1f, 1f) }
}
// Определяем целевое состояние: true = круг (scrollOffset >= 0), false = квадрат (scrollOffset
// < snapThreshold)
val targetIsCircle by remember {
derivedStateOf { derivedStateOf {
// Решение: круг если scrollOffset >= snapThreshold (50% от минимального) if (!isDragging && overscrollOffset > snapThreshold) maxOverscroll
scrollOffset >= snapThreshold else if (!isDragging) 0f else overscrollOffset
} }
} }
// Текущее отображаемое состояние (для предотвращения мерцания) // Пружинистая анимация для snap-эффекта - быстрая
var displayedIsCircle by remember { mutableStateOf(true) } val animatedOverscroll by
animateFloatAsState(
// Overscroll progress - просто на основе scrollOffset targetValue = snapTarget,
val overscrollProgress by remember { animationSpec =
derivedStateOf { spring(
// Если мы в режиме "круг", всегда показываем 0 (полный круг) dampingRatio = Spring.DampingRatioLowBouncy,
// Если в режиме "квадрат", показываем 1 (полный квадрат) stiffness = 2000f // Очень быстрая анимация
val rawProgress = ),
if (animatedScrollOffset.value < 0f) { label = "overscroll"
(-animatedScrollOffset.value / (-minScrollOffset)).coerceIn(0f, 1f)
} else {
0f
}
// Дискретный прогресс на основе displayedIsCircle
val discreteProgress = if (displayedIsCircle) 0f else 1f
Log.d(
TAG,
"📊 overscrollProgress: raw=$rawProgress, discrete=$discreteProgress, displayedIsCircle=$displayedIsCircle, targetIsCircle=$targetIsCircle, scrollOffset=${animatedScrollOffset.value}"
)
discreteProgress
}
}
// Обновляем displayedIsCircle только при отпускании пальца (snap)
LaunchedEffect(isDragging, targetIsCircle) {
Log.d(
TAG,
"🎯 LaunchedEffect: isDragging=$isDragging, targetIsCircle=$targetIsCircle, displayedIsCircle=$displayedIsCircle, scrollOffset=$scrollOffset, lastScrollDirectionUp=$lastScrollDirectionUp"
)
if (!isDragging) {
// Палец отпущен - snap на основе направления скролла
// Если скроллили ВВЕРХ - snap к кругу, если ВНИЗ - snap к квадрату
val newIsCircle = lastScrollDirectionUp || scrollOffset >= 0f
Log.d(
TAG,
"👆 Finger released: scrollOffset=$scrollOffset, lastScrollDirectionUp=$lastScrollDirectionUp, newIsCircle=$newIsCircle"
) )
if (scrollOffset < 0f) { // Calculate expansion progress (0 = круг, 1 = квадрат) - только когда header раскрыт
// Были в overscroll зоне val expansionProgress by remember {
val targetOffset = if (newIsCircle) 0f else minScrollOffset derivedStateOf {
Log.d( if (collapseProgress > 0.1f) 0f // Не расширяем если header коллапсирован
TAG, else (animatedOverscroll / maxOverscroll).coerceIn(0f, 1f)
"🎯 Snap animation: from $scrollOffset to $targetOffset (circle=$newIsCircle)"
)
// Сначала обновляем состояние формы
displayedIsCircle = newIsCircle
// Потом анимируем позицию
animatedScrollOffset.animateTo(
targetValue = targetOffset,
animationSpec =
spring(
dampingRatio =
androidx.compose.animation.core.Spring
.DampingRatioMediumBouncy,
stiffness =
androidx.compose.animation.core.Spring.StiffnessLow
)
)
scrollOffset = targetOffset
} else if (scrollOffset >= 0f) {
// Обычный скролл - всегда круг
displayedIsCircle = true
animatedScrollOffset.snapTo(scrollOffset)
}
} }
} }
// Синхронизация scrollOffset -> animatedScrollOffset во время drag // Nested scroll connection для collapsing + overscroll
LaunchedEffect(scrollOffset, isDragging) {
if (isDragging) {
animatedScrollOffset.snapTo(scrollOffset)
Log.d(TAG, "🔄 Sync during drag: scrollOffset=$scrollOffset -> animatedScrollOffset")
}
}
// Nested scroll connection for tracking scroll with overscroll support
val nestedScrollConnection = remember { val nestedScrollConnection = remember {
object : NestedScrollConnection { object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y val delta = available.y
val newOffset = scrollOffset - delta
// Отслеживаем, что палец активен
val wasDragging = isDragging
isDragging = true isDragging = true
// Отслеживаем направление скролла (только значимые движения) // Если тянем вверх (delta < 0)
if (kotlin.math.abs(delta) > 3f) { if (delta < 0) {
// delta < 0 = палец идёт вверх (скролл контента вниз, возврат к кругу) // Сначала убираем overscroll если есть
// delta > 0 = палец идёт вниз (скролл контента вверх, к квадрату) if (overscrollOffset > 0) {
lastScrollDirectionUp = delta < 0f val newOffset = (overscrollOffset + delta).coerceAtLeast(0f)
Log.d( val consumed = overscrollOffset - newOffset
TAG, overscrollOffset = newOffset
"🧭 Direction changed: delta=$delta, lastScrollDirectionUp=$lastScrollDirectionUp" return Offset(0f, -consumed)
) }
} // Затем коллапсируем header
if (scrollOffset < maxScrollOffset) {
Log.d( val newScrollOffset = (scrollOffset - delta).coerceIn(0f, maxScrollOffset)
TAG, val consumed = newScrollOffset - scrollOffset
"🔄 onPreScroll: delta=$delta, scrollOffset=$scrollOffset, newOffset=$newOffset, wasDragging=$wasDragging, displayedIsCircle=$displayedIsCircle, lastScrollDirectionUp=$lastScrollDirectionUp" scrollOffset = newScrollOffset
) return Offset(0f, -consumed)
// Если скроллим вверх (delta < 0) и были в overscroll зоне - возвращаемся к кругу
if (delta < 0f && scrollOffset < 0f) {
// Плавно уменьшаем overscroll
val newValue = (scrollOffset - delta).coerceAtMost(0f)
scrollOffset = newValue
// Когда достигли 0, переключаемся на круг
if (scrollOffset >= 0f) {
Log.d(TAG, "⚡ Reached 0 from overscroll - switching to circle")
displayedIsCircle = true
scrollOffset = 0f
} }
Log.d(TAG, "📤 Scroll up in overscroll: scrollOffset=$scrollOffset")
return Offset(0f, -delta)
} }
val consumed = // Если тянем вниз (delta > 0) и header коллапсирован - сначала раскрываем
when { if (delta > 0 && scrollOffset > 0) {
// Scroll up (collapse) - delta < 0 = палец идёт вверх val newScrollOffset = (scrollOffset - delta).coerceAtLeast(0f)
delta < 0 && scrollOffset < maxScrollOffset -> { val consumed = scrollOffset - newScrollOffset
val consumed = scrollOffset = newScrollOffset
(newOffset.coerceIn(minScrollOffset, maxScrollOffset) - return Offset(0f, consumed)
scrollOffset) }
scrollOffset = newOffset.coerceIn(minScrollOffset, maxScrollOffset)
Log.d(TAG, "📈 Collapse: scrollOffset=$scrollOffset") return Offset.Zero
-consumed }
}
// Scroll down (expand / overscroll) - delta > 0 = палец идёт вниз override fun onPostScroll(
delta > 0 && scrollOffset > minScrollOffset -> { consumed: Offset,
val consumed = available: Offset,
scrollOffset - source: NestedScrollSource
newOffset.coerceIn(minScrollOffset, maxScrollOffset) ): Offset {
scrollOffset = newOffset.coerceIn(minScrollOffset, maxScrollOffset) // Если достигли верха (scrollOffset = 0) и тянем вниз - создаем overscroll
Log.d(TAG, "📉 Expand/Overscroll: scrollOffset=$scrollOffset") if (available.y > 0 && scrollOffset == 0f) {
consumed val resistance = 0.3f
} val delta = available.y * resistance
else -> 0f overscrollOffset = (overscrollOffset + delta).coerceAtMost(maxOverscroll)
} return Offset(0f, available.y)
return Offset(0f, consumed) }
return Offset.Zero
} }
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
Log.d(
TAG,
"👆 onPostFling: scrollOffset=$scrollOffset, displayedIsCircle=$displayedIsCircle, lastScrollDirectionUp=$lastScrollDirectionUp"
)
isDragging = false isDragging = false
Log.d(TAG, "👆 onPostFling: drag ended, isDragging=false, triggering snap") return Velocity.Zero
return super.onPostFling(consumed, available)
} }
} }
} }
@@ -541,21 +457,7 @@ fun ProfileScreen(
.nestedScroll(nestedScrollConnection) .nestedScroll(nestedScrollConnection)
) { ) {
// Scrollable content // Scrollable content
LazyColumn( LazyColumn(modifier = Modifier.fillMaxSize().padding(top = headerHeight)) {
modifier =
Modifier.fillMaxSize()
.padding(
top =
with(density) {
// Не увеличиваем padding при overscroll
// Используем animatedScrollOffset для плавности
(expandedHeightPx -
animatedScrollOffset.value
.coerceAtLeast(0f))
.toDp()
}
)
) {
item { item {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@@ -666,7 +568,7 @@ fun ProfileScreen(
} }
// ═════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════
// 🎨 COLLAPSING HEADER - Telegram style // 🎨 COLLAPSING PROFILE HEADER with overscroll expansion
// ═════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════
CollapsingProfileHeader( CollapsingProfileHeader(
name = editedName.ifBlank { accountPublicKey.take(10) }, name = editedName.ifBlank { accountPublicKey.take(10) },
@@ -674,7 +576,7 @@ fun ProfileScreen(
publicKey = accountPublicKey, publicKey = accountPublicKey,
avatarColors = avatarColors, avatarColors = avatarColors,
collapseProgress = collapseProgress, collapseProgress = collapseProgress,
overscrollProgress = overscrollProgress, expansionProgress = expansionProgress,
onBack = onBack, onBack = onBack,
hasChanges = hasChanges, hasChanges = hasChanges,
onSave = { onSave = {
@@ -803,7 +705,10 @@ fun ProfileScreen(
} }
// ═════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════
// 🎯 COLLAPSING PROFILE HEADER - Telegram Style Animation // 🎯 COLLAPSING PROFILE HEADER - Circle by default, Square on overscroll
// По умолчанию: круглая аватарка
// При overscroll вниз (expansionProgress): расширяется до квадрата
// При скролле вверх (collapseProgress): уменьшается и уходит вверх
// ═════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════
@Composable @Composable
private fun CollapsingProfileHeader( private fun CollapsingProfileHeader(
@@ -812,7 +717,7 @@ private fun CollapsingProfileHeader(
publicKey: String, publicKey: String,
avatarColors: AvatarColors, avatarColors: AvatarColors,
collapseProgress: Float, collapseProgress: Float,
overscrollProgress: Float, // 0 = normal, 1 = full overscroll (квадратный аватар) expansionProgress: Float,
onBack: () -> Unit, onBack: () -> Unit,
hasChanges: Boolean, hasChanges: Boolean,
onSave: () -> Unit, onSave: () -> Unit,
@@ -831,140 +736,118 @@ private fun CollapsingProfileHeader(
// Get actual status bar height // Get actual status bar height
val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
// Header heights // ═══════════════════════════════════════════════════════════
// По умолчанию header = ширина экрана минус отступ для Account, при скролле уменьшается // 📐 HEADER HEIGHT - ФИКСИРОВАННАЯ! Не меняется при overscroll
val expandedHeight = // ═══════════════════════════════════════════════════════════
screenWidthDp - 60.dp // Высота header (меньше чтобы не перекрывать Account) val expandedHeight = EXPANDED_HEADER_HEIGHT + statusBarHeight
val collapsedHeight = COLLAPSED_HEADER_HEIGHT + statusBarHeight val collapsedHeight = COLLAPSED_HEADER_HEIGHT + statusBarHeight
// Animated header height - НЕ увеличивается при overscroll // Header height меняется только при collapse, НЕ при overscroll
val headerHeight = val headerHeight =
androidx.compose.ui.unit.lerp( androidx.compose.ui.unit.lerp(expandedHeight, collapsedHeight, collapseProgress)
expandedHeight,
collapsedHeight,
collapseProgress.coerceAtLeast(0f)
)
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// 👤 AVATAR - По умолчанию круглый, при overscroll становится квадратным // 👤 AVATAR - По умолчанию КРУГЛАЯ, при overscroll расширяется до прямоугольника
// Аватар всегда ограничен размером header // При collapse - уменьшается и уходит вверх
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// Размер круглого аватара по умолчанию
val circleSize = AVATAR_SIZE_EXPANDED val circleSize = AVATAR_SIZE_EXPANDED
// Зона аватарки = ВСЯ высота header включая статус бар
val avatarZoneHeight = EXPANDED_HEADER_HEIGHT + statusBarHeight
// При overscroll: от круга до полного размера header // При overscroll расширяем до прямоугольника на всю зону (только если не collapsed)
// При collapse: от круга до 0
val avatarWidth: Dp val avatarWidth: Dp
val avatarHeight: Dp val avatarHeight: Dp
val avatarX: Dp
val avatarY: Dp
val cornerRadius: Dp
val collapseOnly = collapseProgress.coerceAtLeast(0f) if (collapseProgress < 0.1f && expansionProgress > 0f) {
// Overscroll: круг -> прямоугольник на всю зону ВКЛЮЧАЯ статус бар
if (overscrollProgress > 0f) { avatarWidth = androidx.compose.ui.unit.lerp(circleSize, screenWidthDp, expansionProgress)
// OVERSCROLL: размер СРАЗУ полный, закругление плавно уменьшается avatarHeight =
avatarWidth = screenWidthDp androidx.compose.ui.unit.lerp(circleSize, avatarZoneHeight, expansionProgress)
avatarHeight = expandedHeight
avatarX = 0.dp
avatarY = 0.dp
// Закругление плавно от круга (circleSize/2) до квадрата (0)
cornerRadius = androidx.compose.ui.unit.lerp(circleSize / 2, 0.dp, overscrollProgress)
} else { } else {
// NORMAL / COLLAPSE: круглый аватар уменьшается при скролле // Collapse: сразу начинаем уменьшаться от круга до 0
avatarWidth = androidx.compose.ui.unit.lerp(circleSize, 0.dp, collapseOnly) val collapsedSize = androidx.compose.ui.unit.lerp(circleSize, 0.dp, collapseProgress)
avatarHeight = androidx.compose.ui.unit.lerp(circleSize, 0.dp, collapseOnly) avatarWidth = collapsedSize
avatarX = (screenWidthDp - avatarWidth) / 2 avatarHeight = collapsedSize
// Позиция Y: по центру header при развернутом, уходит вверх при сворачивании
val avatarCenterY = (expandedHeight - circleSize) / 2
avatarY =
if (collapseOnly < 0.5f) {
androidx.compose.ui.unit.lerp(
avatarCenterY,
statusBarHeight + 14.dp,
collapseOnly * 2
)
} else {
val phase2Progress = (collapseOnly - 0.5f) / 0.5f
androidx.compose.ui.unit.lerp(
statusBarHeight + 14.dp,
statusBarHeight - 60.dp,
phase2Progress
)
}
// Полностью круглый
cornerRadius = circleSize / 2
} }
// Для cornerRadius используем меньшую сторону val avatarSize = if (avatarWidth < avatarHeight) avatarWidth else avatarHeight
val avatarSize = minOf(avatarWidth, avatarHeight)
val avatarFontSize = androidx.compose.ui.unit.lerp(40.sp, 0.sp, collapseOnly) // Позиция X: всегда по центру
val avatarX = (screenWidthDp - avatarWidth) / 2
// Позиция Y
val availableHeight = avatarZoneHeight - statusBarHeight
val defaultCenterY = statusBarHeight + (availableHeight - avatarHeight) / 2
val topAvatarY = 0.dp // От самого верха экрана при полном expansion
val avatarY =
if (collapseProgress < 0.1f && expansionProgress > 0f) {
// При overscroll прижимаемся к самому верху
androidx.compose.ui.unit.lerp(defaultCenterY, topAvatarY, expansionProgress)
} else {
// Collapse: сразу начинаем уходить вверх
androidx.compose.ui.unit.lerp(
defaultCenterY,
statusBarHeight - 80.dp,
collapseProgress
)
}
// Закругление: круг по умолчанию, при overscroll становится квадратом
val cornerRadius =
if (collapseProgress < 0.1f && expansionProgress > 0f) {
// Overscroll: круг -> квадрат с небольшим скруглением
androidx.compose.ui.unit.lerp(avatarSize / 2, 12.dp, expansionProgress)
} else {
// Всегда круг
avatarSize / 2
}
val avatarFontSize = androidx.compose.ui.unit.lerp(40.sp, 0.sp, collapseProgress)
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// 📝 TEXT - always centered, under avatar // 📝 TEXT - внизу header зоны, внутри блока
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
val textX = screenWidthDp / 2 // Always center val textDefaultY = expandedHeight - 60.dp // Внизу header блока (чуть ниже)
// Позиция Y аватара для расчета текста (используем expandedHeight для стабильности)
val avatarCenterYForText = (expandedHeight - circleSize) / 2
// Позиция текста: под аватаром по центру
val avatarBottomY = avatarCenterYForText + circleSize // Низ аватара
val textExpandedY = avatarBottomY + 16.dp // 16dp отступ от аватара
val textCollapsedY = statusBarHeight + COLLAPSED_HEADER_HEIGHT / 2 val textCollapsedY = statusBarHeight + COLLAPSED_HEADER_HEIGHT / 2
val textY = androidx.compose.ui.unit.lerp(textExpandedY, textCollapsedY, collapseOnly)
// Текст меняет позицию только при collapse, НЕ при overscroll
val textY = androidx.compose.ui.unit.lerp(textDefaultY, textCollapsedY, collapseProgress)
// Font sizes // Font sizes
val nameFontSize = androidx.compose.ui.unit.lerp(24.sp, 18.sp, collapseOnly) val nameFontSize = androidx.compose.ui.unit.lerp(24.sp, 18.sp, collapseProgress)
val onlineFontSize = androidx.compose.ui.unit.lerp(14.sp, 13.sp, collapseOnly) val onlineFontSize = androidx.compose.ui.unit.lerp(14.sp, 13.sp, collapseProgress)
Box( Box(modifier = Modifier.fillMaxWidth().height(headerHeight)) {
modifier =
Modifier.fillMaxWidth()
.height(headerHeight)
.clip(
RoundedCornerShape(0.dp)
) // Обрезаем содержимое по границам header
) {
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// 🎨 BLURRED AVATAR BACKGROUND - скрываем при overscroll (аватар сам закрывает фон) // 🎨 BLURRED AVATAR BACKGROUND - всегда показываем
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
if (overscrollProgress < 0.5f) { BlurredAvatarBackground(
BlurredAvatarBackground( publicKey = publicKey,
publicKey = publicKey, avatarRepository = avatarRepository,
avatarRepository = avatarRepository, fallbackColor = avatarColors.backgroundColor,
fallbackColor = avatarColors.backgroundColor, blurRadius = 25f,
blurRadius = 25f, alpha = 0.3f
alpha = 0.3f )
)
}
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// 👤 AVATAR - По умолчанию круглый по центру // 👤 AVATAR - Круг по умолчанию, квадрат при overscroll
// РИСУЕМ ПЕРВЫМ чтобы кнопки были поверх
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
if (avatarSize > 1.dp) { if (avatarSize > 1.dp) {
Box( Box(
modifier = modifier =
Modifier.offset(x = avatarX, y = avatarY) Modifier.offset(x = avatarX, y = avatarY)
.width(avatarWidth) .size(width = avatarWidth, height = avatarHeight)
.height(avatarHeight)
.clip(RoundedCornerShape(cornerRadius)), .clip(RoundedCornerShape(cornerRadius)),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
// Используем AvatarImage если репозиторий доступен
if (avatarRepository != null) { if (avatarRepository != null) {
// Всегда используем FullSizeAvatar чтобы избежать мерцания при переключении
FullSizeAvatar( FullSizeAvatar(
publicKey = publicKey, publicKey = publicKey,
avatarRepository = avatarRepository, avatarRepository = avatarRepository,
isDarkTheme = isDarkTheme isDarkTheme = isDarkTheme
) )
} else { } else {
// Fallback: цветной placeholder с инициалами
Box( Box(
modifier = modifier =
Modifier.fillMaxSize().background(avatarColors.backgroundColor), Modifier.fillMaxSize().background(avatarColors.backgroundColor),
@@ -984,7 +867,7 @@ private fun CollapsingProfileHeader(
} }
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// 🔙 BACK BUTTON (поверх аватара) // 🔙 BACK BUTTON
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
Box( Box(
modifier = modifier =
@@ -1004,8 +887,7 @@ private fun CollapsingProfileHeader(
} }
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// ⋮ MENU BUTTON / 💾 SAVE BUTTON (top right corner) // ⋮ MENU BUTTON / 💾 SAVE BUTTON
// Показываем Save если есть изменения, иначе три точки меню
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
Box( Box(
modifier = modifier =
@@ -1014,7 +896,6 @@ private fun CollapsingProfileHeader(
.padding(end = 4.dp, top = 4.dp), .padding(end = 4.dp, top = 4.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
// Save button (when has changes)
AnimatedVisibility(visible = hasChanges, enter = fadeIn(), exit = fadeOut()) { AnimatedVisibility(visible = hasChanges, enter = fadeIn(), exit = fadeOut()) {
TextButton(onClick = onSave) { TextButton(onClick = onSave) {
Text( Text(
@@ -1025,7 +906,6 @@ private fun CollapsingProfileHeader(
} }
} }
// Menu button (when no changes)
AnimatedVisibility(visible = !hasChanges, enter = fadeIn(), exit = fadeOut()) { AnimatedVisibility(visible = !hasChanges, enter = fadeIn(), exit = fadeOut()) {
IconButton( IconButton(
onClick = { onAvatarMenuChange(true) }, onClick = { onAvatarMenuChange(true) },
@@ -1034,13 +914,12 @@ private fun CollapsingProfileHeader(
Icon( Icon(
imageVector = TablerIcons.DotsVertical, imageVector = TablerIcons.DotsVertical,
contentDescription = "Profile menu", contentDescription = "Profile menu",
tint = Color.White, // Всегда белые - на фоне аватара tint = Color.White,
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )
} }
} }
// Меню для установки фото профиля
com.rosetta.messenger.ui.chats.components.ProfilePhotoMenu( com.rosetta.messenger.ui.chats.components.ProfilePhotoMenu(
expanded = showAvatarMenu, expanded = showAvatarMenu,
onDismiss = { onAvatarMenuChange(false) }, onDismiss = { onAvatarMenuChange(false) },
@@ -1058,7 +937,7 @@ private fun CollapsingProfileHeader(
} }
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// 📝 TEXT BLOCK - Name + Online, always centered // 📝 TEXT BLOCK - Name + Online
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
Column( Column(
modifier = modifier =
@@ -1090,7 +969,6 @@ private fun CollapsingProfileHeader(
Spacer(modifier = Modifier.height(2.dp)) Spacer(modifier = Modifier.height(2.dp))
// Online text - always centered
Text(text = "online", fontSize = onlineFontSize, color = Color(0xFF4CAF50)) Text(text = "online", fontSize = onlineFontSize, color = Color(0xFF4CAF50))
} }
} }