feat: Implement multi-image editing with captions in ChatDetailScreen and enhance ProfileScreen with overscroll effects
This commit is contained in:
@@ -32,11 +32,13 @@ import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
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.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -644,20 +646,44 @@ private fun CollapsingProfileHeader(
|
||||
val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
|
||||
// Header heights
|
||||
val expandedHeight = EXPANDED_HEADER_HEIGHT + statusBarHeight
|
||||
// По умолчанию header = ширина экрана минус отступ для Account, при скролле уменьшается
|
||||
val expandedHeight = screenWidthDp - 60.dp // Высота header (меньше чтобы не перекрывать Account)
|
||||
val collapsedHeight = COLLAPSED_HEADER_HEIGHT + statusBarHeight
|
||||
|
||||
// Animated header height
|
||||
val headerHeight = androidx.compose.ui.unit.lerp(expandedHeight, collapsedHeight, collapseProgress)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 👤 AVATAR - shrinks and moves UP until disappears
|
||||
// 👤 AVATAR - По умолчанию квадратный во весь экран, при скролле становится круглым
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Размер: ширина = screenWidthDp (полная ширина), высота = expandedHeight
|
||||
// При скролле уменьшается до 0
|
||||
val avatarWidth = androidx.compose.ui.unit.lerp(screenWidthDp, 0.dp, collapseProgress)
|
||||
val avatarHeight = androidx.compose.ui.unit.lerp(expandedHeight, 0.dp, collapseProgress)
|
||||
|
||||
// Для cornerRadius и других расчётов используем меньшую сторону
|
||||
val avatarSize = minOf(avatarWidth, avatarHeight)
|
||||
|
||||
// Позиция X: от 0 (весь экран) до центра
|
||||
val avatarCenterX = (screenWidthDp - AVATAR_SIZE_EXPANDED) / 2
|
||||
val avatarStartY = statusBarHeight + 32.dp
|
||||
val avatarEndY = statusBarHeight - 60.dp // Moves above screen
|
||||
val avatarY = androidx.compose.ui.unit.lerp(avatarStartY, avatarEndY, collapseProgress)
|
||||
val avatarSize = androidx.compose.ui.unit.lerp(AVATAR_SIZE_EXPANDED, 0.dp, collapseProgress)
|
||||
val avatarX = androidx.compose.ui.unit.lerp(0.dp, avatarCenterX + (AVATAR_SIZE_EXPANDED - avatarSize) / 2, collapseProgress.coerceIn(0f, 0.5f) * 2f)
|
||||
|
||||
// Позиция Y: от 0 (верх экрана) до обычной позиции, потом уходит вверх
|
||||
val avatarStartY = 0.dp // Начинаем с самого верха (закрываем status bar)
|
||||
val avatarMidY = statusBarHeight + 32.dp // Средняя позиция (круглый аватар)
|
||||
val avatarEndY = statusBarHeight - 60.dp // Уходит вверх за экран
|
||||
|
||||
val avatarY = if (collapseProgress < 0.5f) {
|
||||
// Первая половина: от 0 до обычной позиции
|
||||
androidx.compose.ui.unit.lerp(avatarStartY, avatarMidY, collapseProgress * 2f)
|
||||
} else {
|
||||
// Вторая половина: уходит вверх
|
||||
androidx.compose.ui.unit.lerp(avatarMidY, avatarEndY, (collapseProgress - 0.5f) * 2f)
|
||||
}
|
||||
|
||||
// Закругление: от 0 (квадрат) до половины размера (круг)
|
||||
val cornerRadius = androidx.compose.ui.unit.lerp(0.dp, avatarSize / 2, collapseProgress.coerceIn(0f, 0.3f) / 0.3f)
|
||||
|
||||
val avatarFontSize = androidx.compose.ui.unit.lerp(40.sp, 0.sp, collapseProgress)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
@@ -679,101 +705,53 @@ private fun CollapsingProfileHeader(
|
||||
.height(headerHeight)
|
||||
) {
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🎨 BLURRED AVATAR BACKGROUND (вместо цвета)
|
||||
// 🎨 BLURRED AVATAR BACKGROUND - только когда аватар уже круглый
|
||||
// При квадратном аватаре фон не нужен (аватар сам занимает весь header)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
BlurredAvatarBackground(
|
||||
publicKey = publicKey,
|
||||
avatarRepository = avatarRepository,
|
||||
fallbackColor = avatarColors.backgroundColor,
|
||||
blurRadius = 25f,
|
||||
alpha = 0.3f
|
||||
)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🔙 BACK BUTTON
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = statusBarHeight)
|
||||
.padding(start = 4.dp, top = 4.dp)
|
||||
.size(48.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onBack,
|
||||
modifier = Modifier.size(48.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = TablerIcons.ArrowLeft,
|
||||
contentDescription = "Back",
|
||||
tint = if (isColorLight(avatarColors.backgroundColor)) Color.Black else Color.White,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// ⋮ MENU BUTTON (top right corner)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(top = statusBarHeight)
|
||||
.padding(end = 4.dp, top = 4.dp)
|
||||
.size(48.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { onAvatarMenuChange(true) },
|
||||
modifier = Modifier.size(48.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = TablerIcons.DotsVertical,
|
||||
contentDescription = "Profile menu",
|
||||
tint = if (isColorLight(avatarColors.backgroundColor)) Color.Black else Color.White,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Меню для установки фото профиля
|
||||
com.rosetta.messenger.ui.chats.components.ProfilePhotoMenu(
|
||||
expanded = showAvatarMenu,
|
||||
onDismiss = { onAvatarMenuChange(false) },
|
||||
isDarkTheme = isDarkTheme,
|
||||
onSetPhotoClick = {
|
||||
onAvatarMenuChange(false)
|
||||
onSetPhotoClick()
|
||||
}
|
||||
if (collapseProgress > 0.3f) {
|
||||
BlurredAvatarBackground(
|
||||
publicKey = publicKey,
|
||||
avatarRepository = avatarRepository,
|
||||
fallbackColor = avatarColors.backgroundColor,
|
||||
blurRadius = 25f,
|
||||
alpha = 0.3f * ((collapseProgress - 0.3f) / 0.7f).coerceIn(0f, 1f) // Плавное появление
|
||||
)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 👤 AVATAR - shrinks and moves up (with real avatar support)
|
||||
// <EFBFBD> AVATAR - По умолчанию квадратный, при скролле становится круглым
|
||||
// РИСУЕМ ПЕРВЫМ чтобы кнопки были поверх
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
if (avatarSize > 1.dp) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.offset(
|
||||
x = avatarCenterX + (AVATAR_SIZE_EXPANDED - avatarSize) / 2,
|
||||
x = avatarX,
|
||||
y = avatarY
|
||||
)
|
||||
.size(avatarSize)
|
||||
.clip(CircleShape)
|
||||
.background(Color.White.copy(alpha = 0.15f))
|
||||
.padding(2.dp)
|
||||
.clip(CircleShape),
|
||||
.width(avatarWidth)
|
||||
.height(avatarHeight)
|
||||
.clip(RoundedCornerShape(cornerRadius)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// Используем AvatarImage если репозиторий доступен
|
||||
if (avatarRepository != null) {
|
||||
AvatarImage(
|
||||
publicKey = publicKey,
|
||||
avatarRepository = avatarRepository,
|
||||
size = avatarSize - 4.dp,
|
||||
isDarkTheme = false, // Header всегда светлый на цветном фоне
|
||||
onClick = null,
|
||||
showOnlineIndicator = false
|
||||
)
|
||||
// При collapseProgress < 0.2 - fullscreen аватар
|
||||
if (collapseProgress < 0.2f) {
|
||||
FullSizeAvatar(
|
||||
publicKey = publicKey,
|
||||
avatarRepository = avatarRepository
|
||||
)
|
||||
} else {
|
||||
AvatarImage(
|
||||
publicKey = publicKey,
|
||||
avatarRepository = avatarRepository,
|
||||
size = avatarSize - 4.dp,
|
||||
isDarkTheme = false,
|
||||
onClick = null,
|
||||
showOnlineIndicator = false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Fallback: цветной placeholder с инициалами
|
||||
Box(
|
||||
@@ -795,6 +773,64 @@ private fun CollapsingProfileHeader(
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🔙 BACK BUTTON (поверх аватара)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = statusBarHeight)
|
||||
.padding(start = 4.dp, top = 4.dp)
|
||||
.size(48.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onBack,
|
||||
modifier = Modifier.size(48.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = TablerIcons.ArrowLeft,
|
||||
contentDescription = "Back",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// ⋮ MENU BUTTON (top right corner, поверх аватара)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(top = statusBarHeight)
|
||||
.padding(end = 4.dp, top = 4.dp)
|
||||
.size(48.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { onAvatarMenuChange(true) },
|
||||
modifier = Modifier.size(48.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = TablerIcons.DotsVertical,
|
||||
contentDescription = "Profile menu",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Меню для установки фото профиля
|
||||
com.rosetta.messenger.ui.chats.components.ProfilePhotoMenu(
|
||||
expanded = showAvatarMenu,
|
||||
onDismiss = { onAvatarMenuChange(false) },
|
||||
isDarkTheme = isDarkTheme,
|
||||
onSetPhotoClick = {
|
||||
onAvatarMenuChange(false)
|
||||
onSetPhotoClick()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 📝 TEXT BLOCK - Name + Online, always centered
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
@@ -855,7 +891,55 @@ private fun CollapsingProfileHeader(
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
// 📦 PROFILE CARD COMPONENT - Legacy (kept for OtherProfileScreen)
|
||||
// <EFBFBD> FULL SIZE AVATAR - Fills entire container (for expanded state)
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
@Composable
|
||||
private fun FullSizeAvatar(
|
||||
publicKey: String,
|
||||
avatarRepository: AvatarRepository?
|
||||
) {
|
||||
val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
|
||||
?: remember { mutableStateOf(emptyList()) }
|
||||
|
||||
var bitmap by remember(avatars) { mutableStateOf<android.graphics.Bitmap?>(null) }
|
||||
|
||||
LaunchedEffect(avatars) {
|
||||
bitmap = if (avatars.isNotEmpty()) {
|
||||
kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
|
||||
com.rosetta.messenger.utils.AvatarFileManager.base64ToBitmap(avatars.first().base64Data)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (bitmap != null) {
|
||||
Image(
|
||||
bitmap = bitmap!!.asImageBitmap(),
|
||||
contentDescription = "Avatar",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
} else {
|
||||
val avatarColors = getAvatarColor(publicKey, false)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(avatarColors.backgroundColor),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = publicKey.take(2).uppercase(),
|
||||
color = avatarColors.textColor,
|
||||
fontSize = 80.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
// <20>📦 PROFILE CARD COMPONENT - Legacy (kept for OtherProfileScreen)
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
@Composable
|
||||
fun ProfileCard(
|
||||
|
||||
Reference in New Issue
Block a user