feat: enhance RequestsSection layout and add full-screen avatar viewer in ProfileScreen
This commit is contained in:
@@ -627,6 +627,7 @@ fun ChatsListScreen(
|
|||||||
iconColor = menuIconColor,
|
iconColor = menuIconColor,
|
||||||
textColor = textColor,
|
textColor = textColor,
|
||||||
badge = if (topLevelRequestsCount > 0) topLevelRequestsCount.toString() else null,
|
badge = if (topLevelRequestsCount > 0) topLevelRequestsCount.toString() else null,
|
||||||
|
badgeColor = accentColor,
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
drawerState.close()
|
drawerState.close()
|
||||||
@@ -913,7 +914,7 @@ fun ChatsListScreen(
|
|||||||
.offset(x = 2.dp, y = (-2).dp)
|
.offset(x = 2.dp, y = (-2).dp)
|
||||||
.size(8.dp)
|
.size(8.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(Color(0xFFE53935))
|
.background(if (isDarkTheme) PrimaryBlueDark else PrimaryBlue)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2870,7 +2871,7 @@ fun TypingIndicatorSmall() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 📬 Секция Requests — Telegram-style chat item (как Archived Chats) */
|
/** 📬 Секция Requests — Telegram Archived Chats style */
|
||||||
@Composable
|
@Composable
|
||||||
fun RequestsSection(
|
fun RequestsSection(
|
||||||
count: Int,
|
count: Int,
|
||||||
@@ -2883,6 +2884,7 @@ fun RequestsSection(
|
|||||||
remember(isDarkTheme) { if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) }
|
remember(isDarkTheme) { if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) }
|
||||||
val iconBgColor =
|
val iconBgColor =
|
||||||
remember(isDarkTheme) { if (isDarkTheme) Color(0xFF3A3A3C) else Color(0xFFC7C7CC) }
|
remember(isDarkTheme) { if (isDarkTheme) Color(0xFF3A3A3C) else Color(0xFFC7C7CC) }
|
||||||
|
val accentColor = if (isDarkTheme) PrimaryBlueDark else PrimaryBlue
|
||||||
|
|
||||||
// Последний запрос — показываем имя отправителя как subtitle
|
// Последний запрос — показываем имя отправителя как subtitle
|
||||||
val lastRequest = remember(requests) { requests.firstOrNull() }
|
val lastRequest = remember(requests) { requests.firstOrNull() }
|
||||||
@@ -2899,74 +2901,58 @@ fun RequestsSection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Column {
|
||||||
modifier =
|
Row(
|
||||||
Modifier.fillMaxWidth()
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
// Иконка — круглый аватар как в Telegram Archived Chats
|
|
||||||
Box(
|
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.size(56.dp)
|
Modifier.fillMaxWidth()
|
||||||
.clip(CircleShape)
|
.clickable(onClick = onClick)
|
||||||
.background(iconBgColor),
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
contentAlignment = Alignment.Center
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
// Иконка — круглый аватар как Archived Chats в Telegram
|
||||||
imageVector = TablerIcons.MailForward,
|
Box(
|
||||||
contentDescription = null,
|
modifier =
|
||||||
tint = Color.White,
|
Modifier.size(56.dp)
|
||||||
modifier = Modifier.size(26.dp)
|
.clip(CircleShape)
|
||||||
)
|
.background(iconBgColor),
|
||||||
}
|
contentAlignment = Alignment.Center
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
|
|
||||||
// Текст: название + последний отправитель
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Icon(
|
||||||
text = "Requests",
|
imageVector = TablerIcons.MailForward,
|
||||||
fontWeight = FontWeight.SemiBold,
|
contentDescription = null,
|
||||||
fontSize = 16.sp,
|
tint = Color.White,
|
||||||
color = textColor,
|
modifier = Modifier.size(26.dp)
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subtitle.isNotEmpty()) {
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
// Верхняя строка: название + badge
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = subtitle,
|
text = "Requests",
|
||||||
fontSize = 14.sp,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = secondaryTextColor,
|
fontSize = 16.sp,
|
||||||
|
color = textColor,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Badge с количеством
|
// Badge справа на уровне заголовка
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.defaultMinSize(minWidth = 22.dp, minHeight = 22.dp)
|
.defaultMinSize(minWidth = 22.dp, minHeight = 22.dp)
|
||||||
.clip(RoundedCornerShape(11.dp))
|
.clip(CircleShape)
|
||||||
.background(Color(0xFFE53935)),
|
.background(accentColor),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -2974,37 +2960,33 @@ fun RequestsSection(
|
|||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
modifier = Modifier.padding(horizontal = 5.dp, vertical = 1.dp)
|
lineHeight = 12.sp,
|
||||||
|
modifier = Modifier.padding(horizontal = 5.dp, vertical = 3.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (count > 0) {
|
|
||||||
// Если нет subtitle но есть count
|
// Нижняя строка: subtitle (последний запрос)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
if (subtitle.isNotEmpty()) {
|
||||||
Row(
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Text(
|
||||||
horizontalArrangement = Arrangement.End
|
text = subtitle,
|
||||||
) {
|
fontSize = 14.sp,
|
||||||
Box(
|
color = secondaryTextColor,
|
||||||
modifier =
|
maxLines = 1,
|
||||||
Modifier
|
overflow = TextOverflow.Ellipsis
|
||||||
.defaultMinSize(minWidth = 22.dp, minHeight = 22.dp)
|
)
|
||||||
.clip(RoundedCornerShape(11.dp))
|
|
||||||
.background(Color(0xFFE53935)),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = if (count > 99) "99+" else count.toString(),
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = Color.White,
|
|
||||||
modifier = Modifier.padding(horizontal = 5.dp, vertical = 1.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Разделитель как у обычных чатов
|
||||||
|
Divider(
|
||||||
|
color = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8),
|
||||||
|
thickness = 0.5.dp,
|
||||||
|
modifier = Modifier.padding(start = 84.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3208,6 +3190,7 @@ fun DrawerMenuItemEnhanced(
|
|||||||
iconColor: Color,
|
iconColor: Color,
|
||||||
textColor: Color,
|
textColor: Color,
|
||||||
badge: String? = null,
|
badge: String? = null,
|
||||||
|
badgeColor: Color = Color(0xFFE53935),
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -3237,17 +3220,21 @@ fun DrawerMenuItemEnhanced(
|
|||||||
badge?.let {
|
badge?.let {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.background(
|
Modifier
|
||||||
color = Color(0xFFE53935),
|
.defaultMinSize(minWidth = 22.dp, minHeight = 22.dp)
|
||||||
shape = RoundedCornerShape(10.dp)
|
.background(
|
||||||
)
|
color = badgeColor,
|
||||||
.padding(horizontal = 8.dp, vertical = 2.dp)
|
shape = CircleShape
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = it,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Bold,
|
||||||
color = Color.White
|
color = Color.White,
|
||||||
|
lineHeight = 12.sp,
|
||||||
|
modifier = Modifier.padding(horizontal = 5.dp, vertical = 3.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ fun RequestsListScreen(
|
|||||||
val blockedUsers by chatsViewModel.blockedUsers.collectAsState()
|
val blockedUsers by chatsViewModel.blockedUsers.collectAsState()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF2F2F7)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF2F2F7)
|
||||||
|
val headerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4)
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -61,15 +62,16 @@ fun RequestsListScreen(
|
|||||||
text = "Requests",
|
text = "Requests",
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 20.sp,
|
fontSize = 20.sp,
|
||||||
color = textColor
|
color = Color.White
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
colors =
|
colors =
|
||||||
TopAppBarDefaults.topAppBarColors(
|
TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = backgroundColor,
|
containerColor = headerColor,
|
||||||
scrolledContainerColor = backgroundColor,
|
scrolledContainerColor = headerColor,
|
||||||
navigationIconContentColor = textColor,
|
navigationIconContentColor = Color.White,
|
||||||
titleContentColor = textColor
|
titleContentColor = Color.White,
|
||||||
|
actionIconContentColor = Color.White
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -77,20 +77,28 @@ fun BoxScope.BlurredAvatarBackground(
|
|||||||
val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
|
val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
|
||||||
?: remember { mutableStateOf(emptyList()) }
|
?: remember { mutableStateOf(emptyList()) }
|
||||||
|
|
||||||
var originalBitmap by remember(avatars) { mutableStateOf<Bitmap?>(null) }
|
// Stable key based on content, not list reference — prevents bitmap reset during recomposition
|
||||||
var blurredBitmap by remember(avatars) { mutableStateOf<Bitmap?>(null) }
|
val avatarKey = remember(avatars) {
|
||||||
|
avatars.firstOrNull()?.timestamp ?: 0L
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(avatars) {
|
// Don't reset bitmap to null when key changes — keep showing old blur until new one is ready
|
||||||
if (avatars.isNotEmpty()) {
|
var originalBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
originalBitmap = withContext(Dispatchers.IO) {
|
var blurredBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
AvatarFileManager.base64ToBitmap(avatars.first().base64Data)
|
|
||||||
|
LaunchedEffect(avatarKey) {
|
||||||
|
val currentAvatars = avatars
|
||||||
|
if (currentAvatars.isNotEmpty()) {
|
||||||
|
val newOriginal = withContext(Dispatchers.IO) {
|
||||||
|
AvatarFileManager.base64ToBitmap(currentAvatars.first().base64Data)
|
||||||
}
|
}
|
||||||
originalBitmap?.let { bitmap ->
|
if (newOriginal != null) {
|
||||||
|
originalBitmap = newOriginal
|
||||||
blurredBitmap = withContext(Dispatchers.Default) {
|
blurredBitmap = withContext(Dispatchers.Default) {
|
||||||
val scaledBitmap = Bitmap.createScaledBitmap(
|
val scaledBitmap = Bitmap.createScaledBitmap(
|
||||||
bitmap,
|
newOriginal,
|
||||||
bitmap.width / 4,
|
newOriginal.width / 4,
|
||||||
bitmap.height / 4,
|
newOriginal.height / 4,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
var result = scaledBitmap
|
var result = scaledBitmap
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
@@ -368,17 +369,15 @@ fun OtherProfileScreen(
|
|||||||
else -> 0f
|
else -> 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 Плавная spring анимация для snap
|
// Плавная spring анимация для snap (без bounce для гладкости)
|
||||||
val animatedOverscroll by animateFloatAsState(
|
val animatedOverscroll by animateFloatAsState(
|
||||||
targetValue = targetOverscroll,
|
targetValue = targetOverscroll,
|
||||||
animationSpec = if (isDragging && !isPulledDown) {
|
animationSpec = if (isDragging && !isPulledDown) {
|
||||||
// Без анимации во время drag (до snap)
|
|
||||||
spring(stiffness = Spring.StiffnessHigh)
|
spring(stiffness = Spring.StiffnessHigh)
|
||||||
} else {
|
} else {
|
||||||
// Плавная анимация для snap
|
|
||||||
spring(
|
spring(
|
||||||
dampingRatio = Spring.DampingRatioLowBouncy, // Лёгкий bounce
|
dampingRatio = Spring.DampingRatioNoBouncy,
|
||||||
stiffness = Spring.StiffnessMediumLow // Плавное движение
|
stiffness = Spring.StiffnessMedium
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
label = "overscroll"
|
label = "overscroll"
|
||||||
@@ -1674,47 +1673,118 @@ private fun CollapsingOtherProfileHeader(
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
val textColor = Color.White
|
val textColor = Color.White
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxWidth().height(headerHeight)) {
|
Box(modifier = Modifier.fillMaxWidth().height(headerHeight).clipToBounds()) {
|
||||||
// ═══════════════════════════════════════════════════════════
|
// Expansion fraction — computed early so blur can fade during expansion
|
||||||
// 🎨 BLURRED AVATAR BACKGROUND
|
val expandFractionEarly = expansionProgress.coerceIn(0f, 1f)
|
||||||
// ═══════════════════════════════════════════════════════════
|
val blurAlpha = (1f - expandFractionEarly * 2.5f).coerceIn(0f, 1f)
|
||||||
BlurredAvatarBackground(
|
|
||||||
publicKey = publicKey,
|
|
||||||
avatarRepository = avatarRepository,
|
|
||||||
fallbackColor = avatarColors.backgroundColor,
|
|
||||||
blurRadius = 20f,
|
|
||||||
alpha = 0.9f,
|
|
||||||
overlayColors = com.rosetta.messenger.ui.settings.BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId),
|
|
||||||
isDarkTheme = isDarkTheme
|
|
||||||
)
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// 👤 AVATAR with METABALL EFFECT - Liquid merge animation
|
// 🎨 BLURRED AVATAR BACKGROUND - fades out during expansion
|
||||||
// При скролле вверх аватарка "сливается" с Dynamic Island
|
|
||||||
// При свайпе вниз - расширяется на весь экран
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
ProfileMetaballEffect(
|
if (blurAlpha > 0.01f) {
|
||||||
collapseProgress = collapseProgress,
|
Box(modifier = Modifier.matchParentSize().graphicsLayer { alpha = blurAlpha }) {
|
||||||
expansionProgress = expansionProgress,
|
BlurredAvatarBackground(
|
||||||
statusBarHeight = statusBarHeight,
|
|
||||||
headerHeight = headerHeight,
|
|
||||||
hasAvatar = hasAvatar,
|
|
||||||
avatarColor = avatarColors.backgroundColor,
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
) {
|
|
||||||
// Содержимое аватара
|
|
||||||
if (hasAvatar && avatarRepository != null) {
|
|
||||||
OtherProfileFullSizeAvatar(
|
|
||||||
publicKey = publicKey,
|
publicKey = publicKey,
|
||||||
avatarRepository = avatarRepository,
|
avatarRepository = avatarRepository,
|
||||||
|
fallbackColor = avatarColors.backgroundColor,
|
||||||
|
blurRadius = 20f,
|
||||||
|
alpha = 0.9f,
|
||||||
|
overlayColors = com.rosetta.messenger.ui.settings.BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId),
|
||||||
isDarkTheme = isDarkTheme
|
isDarkTheme = isDarkTheme
|
||||||
)
|
)
|
||||||
} else {
|
}
|
||||||
// Placeholder без аватарки
|
}
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxSize().background(avatarColors.backgroundColor),
|
// ═══════════════════════════════════════════════════════════
|
||||||
contentAlignment = Alignment.Center
|
// 👤 AVATAR — Telegram-style expansion on pull-down
|
||||||
|
// При скролле вверх: metaball merge с Dynamic Island
|
||||||
|
// При свайпе вниз: аватарка раскрывается на весь блок (circle → rect)
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
val avatarSize = androidx.compose.ui.unit.lerp(
|
||||||
|
AVATAR_SIZE_EXPANDED_OTHER, AVATAR_SIZE_COLLAPSED_OTHER, collapseProgress
|
||||||
|
)
|
||||||
|
val avatarAlpha = (1f - collapseProgress * 1.8f).coerceIn(0f, 1f)
|
||||||
|
val contentAreaHeight = EXPANDED_HEADER_HEIGHT_OTHER - 70.dp
|
||||||
|
val avatarExpandedY = statusBarHeight + (contentAreaHeight - AVATAR_SIZE_EXPANDED_OTHER) / 2
|
||||||
|
val avatarCollapsedY = statusBarHeight + (COLLAPSED_HEADER_HEIGHT_OTHER - AVATAR_SIZE_COLLAPSED_OTHER) / 2
|
||||||
|
val avatarY = androidx.compose.ui.unit.lerp(avatarExpandedY, avatarCollapsedY, collapseProgress)
|
||||||
|
|
||||||
|
// Telegram-style expansion: circle → full header rect
|
||||||
|
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
|
||||||
|
val expandFraction = expansionProgress.coerceIn(0f, 1f)
|
||||||
|
val expandedAvatarWidth = androidx.compose.ui.unit.lerp(avatarSize, screenWidth, expandFraction)
|
||||||
|
val expandedAvatarHeight = androidx.compose.ui.unit.lerp(avatarSize, headerHeight, expandFraction)
|
||||||
|
val cornerRadius = androidx.compose.ui.unit.lerp(avatarSize / 2, 0.dp, expandFraction)
|
||||||
|
val avatarCenterX = (screenWidth - avatarSize) / 2
|
||||||
|
val expandedAvatarX = androidx.compose.ui.unit.lerp(avatarCenterX, 0.dp, expandFraction)
|
||||||
|
val expandedAvatarY = androidx.compose.ui.unit.lerp(avatarY, 0.dp, expandFraction)
|
||||||
|
|
||||||
|
// Pre-compute pixel values for graphicsLayer (avoids layout-phase recomposition)
|
||||||
|
val expandedAvatarXPx = with(density) { expandedAvatarX.toPx() }
|
||||||
|
val expandedAvatarYPx = with(density) { expandedAvatarY.toPx() }
|
||||||
|
|
||||||
|
// Metaball alpha: visible only when NOT expanding (normal collapse animation)
|
||||||
|
val metaballAlpha = (1f - expandFraction * 10f).coerceIn(0f, 1f)
|
||||||
|
// Expansion avatar alpha: visible when expanding
|
||||||
|
val expansionAvatarAlpha = (expandFraction * 10f).coerceIn(0f, 1f)
|
||||||
|
|
||||||
|
// Layer 1: Metaball effect for normal collapse (fades out when expanding)
|
||||||
|
if (metaballAlpha > 0.01f) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize().graphicsLayer { alpha = metaballAlpha }) {
|
||||||
|
ProfileMetaballEffect(
|
||||||
|
collapseProgress = collapseProgress,
|
||||||
|
expansionProgress = 0f,
|
||||||
|
statusBarHeight = statusBarHeight,
|
||||||
|
headerHeight = headerHeight,
|
||||||
|
hasAvatar = hasAvatar,
|
||||||
|
avatarColor = avatarColors.backgroundColor,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
|
if (hasAvatar && avatarRepository != null) {
|
||||||
|
OtherProfileFullSizeAvatar(
|
||||||
|
publicKey = publicKey,
|
||||||
|
avatarRepository = avatarRepository,
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize().background(avatarColors.backgroundColor),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = getInitials(name),
|
||||||
|
fontSize = avatarFontSize,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = avatarColors.textColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layer 2: Expanding avatar (fades in when pulling down)
|
||||||
|
if (expansionAvatarAlpha > 0.01f) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(width = expandedAvatarWidth, height = expandedAvatarHeight)
|
||||||
|
.graphicsLayer {
|
||||||
|
translationX = expandedAvatarXPx
|
||||||
|
translationY = expandedAvatarYPx
|
||||||
|
alpha = avatarAlpha * expansionAvatarAlpha
|
||||||
|
shape = RoundedCornerShape(cornerRadius)
|
||||||
|
clip = true
|
||||||
|
}
|
||||||
|
.background(avatarColors.backgroundColor),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (hasAvatar && avatarRepository != null) {
|
||||||
|
OtherProfileFullSizeAvatar(
|
||||||
|
publicKey = publicKey,
|
||||||
|
avatarRepository = avatarRepository,
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
|
)
|
||||||
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text = getInitials(name),
|
text = getInitials(name),
|
||||||
fontSize = avatarFontSize,
|
fontSize = avatarFontSize,
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ import androidx.compose.animation.*
|
|||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
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
|
||||||
@@ -25,6 +27,7 @@ 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.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -48,6 +51,7 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.Velocity
|
import androidx.compose.ui.unit.Velocity
|
||||||
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 kotlinx.coroutines.launch
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.palette.graphics.Palette as AndroidPalette
|
import androidx.palette.graphics.Palette as AndroidPalette
|
||||||
import com.rosetta.messenger.biometric.BiometricAuthManager
|
import com.rosetta.messenger.biometric.BiometricAuthManager
|
||||||
@@ -58,8 +62,13 @@ import com.rosetta.messenger.ui.components.BlurredAvatarBackground
|
|||||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlueDark
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlueDark
|
||||||
|
|
||||||
import com.rosetta.messenger.ui.components.metaball.ProfileMetaballEffect
|
|
||||||
import com.rosetta.messenger.utils.AvatarFileManager
|
import com.rosetta.messenger.utils.AvatarFileManager
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.foundation.gestures.detectVerticalDragGestures
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
import com.rosetta.messenger.utils.ImageCropHelper
|
import com.rosetta.messenger.utils.ImageCropHelper
|
||||||
import compose.icons.TablerIcons
|
import compose.icons.TablerIcons
|
||||||
import compose.icons.tablericons.*
|
import compose.icons.tablericons.*
|
||||||
@@ -292,6 +301,11 @@ fun ProfileScreen(
|
|||||||
// 🖼️ Состояние для нашего кастомного photo picker
|
// 🖼️ Состояние для нашего кастомного photo picker
|
||||||
var showPhotoPicker by remember { mutableStateOf(false) }
|
var showPhotoPicker by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// 🔍 Full-screen avatar viewer state
|
||||||
|
var showAvatarViewer by remember { mutableStateOf(false) }
|
||||||
|
var avatarViewerBitmap by remember { mutableStateOf<android.graphics.Bitmap?>(null) }
|
||||||
|
var avatarViewerTimestamp by remember { mutableStateOf(0L) }
|
||||||
|
|
||||||
// URI выбранного изображения (до crop)
|
// URI выбранного изображения (до crop)
|
||||||
var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
|
var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
|
|
||||||
@@ -411,13 +425,28 @@ fun ProfileScreen(
|
|||||||
val collapsedHeightPx = with(density) { (COLLAPSED_HEADER_HEIGHT + statusBarHeight).toPx() }
|
val collapsedHeightPx = with(density) { (COLLAPSED_HEADER_HEIGHT + statusBarHeight).toPx() }
|
||||||
val maxScrollOffset = expandedHeightPx - collapsedHeightPx
|
val maxScrollOffset = expandedHeightPx - collapsedHeightPx
|
||||||
|
|
||||||
// Track scroll offset for collapsing (скролл вверх = collapse)
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// Telegram: extraHeight - напрямую от позиции первого элемента
|
// TELEGRAM ARCHITECTURE: LazyColumn = RecyclerView
|
||||||
var scrollOffset by remember { mutableFloatStateOf(0f) }
|
// Item 0 = spacer высотой с expanded header (как Telegram item 0)
|
||||||
|
// scrollOffset вычисляется из позиции скролла (как Telegram extraHeight)
|
||||||
|
// Контент и хедер двигаются SYNC — один скролл двигает всё
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
val expandedHeaderDp = with(density) { expandedHeightPx.toDp() }
|
||||||
|
|
||||||
|
// Derive scrollOffset from LazyColumn scroll — как Telegram checkListViewScroll()
|
||||||
|
// item 0 top position → extraHeight
|
||||||
|
val scrollOffset by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
if (listState.firstVisibleItemIndex == 0) {
|
||||||
|
listState.firstVisibleItemScrollOffset.toFloat().coerceAtMost(maxScrollOffset)
|
||||||
|
} else {
|
||||||
|
maxScrollOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate collapse progress (0 = expanded, 1 = collapsed)
|
// Calculate collapse progress (0 = expanded, 1 = collapsed)
|
||||||
// Telegram: diff = (extraHeight - actionsHeight) / headerOnlyExtraHeight
|
|
||||||
// Напрямую без derivedStateOf для плавности
|
|
||||||
val collapseProgress = (scrollOffset / maxScrollOffset).coerceIn(0f, 1f)
|
val collapseProgress = (scrollOffset / maxScrollOffset).coerceIn(0f, 1f)
|
||||||
|
|
||||||
// Dynamic header height based on scroll
|
// Dynamic header height based on scroll
|
||||||
@@ -465,17 +494,15 @@ fun ProfileScreen(
|
|||||||
// чтобы аватарка сразу заполнилась после порога
|
// чтобы аватарка сразу заполнилась после порога
|
||||||
val currentProgress = (overscrollOffset / maxOverscroll).coerceIn(0f, 1f)
|
val currentProgress = (overscrollOffset / maxOverscroll).coerceIn(0f, 1f)
|
||||||
|
|
||||||
// 🔥 Плавная spring анимация для snap
|
// Плавная spring анимация для snap (без bounce для гладкости)
|
||||||
val animatedOverscroll by animateFloatAsState(
|
val animatedOverscroll by animateFloatAsState(
|
||||||
targetValue = targetOverscroll,
|
targetValue = targetOverscroll,
|
||||||
animationSpec = if (isDragging && !isPulledDown) {
|
animationSpec = if (isDragging && !isPulledDown) {
|
||||||
// Без анимации во время drag (до snap)
|
|
||||||
spring(stiffness = Spring.StiffnessHigh)
|
spring(stiffness = Spring.StiffnessHigh)
|
||||||
} else {
|
} else {
|
||||||
// Плавная анимация для snap
|
|
||||||
spring(
|
spring(
|
||||||
dampingRatio = Spring.DampingRatioLowBouncy, // Лёгкий bounce
|
dampingRatio = Spring.DampingRatioNoBouncy,
|
||||||
stiffness = Spring.StiffnessMediumLow // Плавное движение
|
stiffness = Spring.StiffnessMedium
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
label = "overscroll"
|
label = "overscroll"
|
||||||
@@ -526,50 +553,32 @@ fun ProfileScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG LOGS
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// NESTED SCROLL - Telegram style
|
// NESTED SCROLL — overscroll (pull-down аватарка) + header snap
|
||||||
|
// Header collapse управляется скроллом LazyColumn (Telegram RecyclerView)
|
||||||
|
// Snap вызывается в onPreFling — СРАЗУ при отпускании пальца
|
||||||
|
// (как Telegram ACTION_UP → smoothScrollBy)
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
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
|
||||||
isDragging = true
|
|
||||||
|
|
||||||
// Тянем вверх (delta < 0)
|
// Тянем вверх (delta < 0) — убираем overscroll
|
||||||
if (delta < 0) {
|
if (delta < 0) {
|
||||||
// Сначала убираем overscroll
|
|
||||||
if (overscrollOffset > 0 || isPulledDown) {
|
if (overscrollOffset > 0 || isPulledDown) {
|
||||||
// 🔥 FIX: Если isPulledDown=true - НЕ меняем overscrollOffset напрямую
|
isDragging = true
|
||||||
// Только сбрасываем isPulledDown и даём анимации плавно свернуть
|
|
||||||
if (isPulledDown) {
|
if (isPulledDown) {
|
||||||
// При достаточном свайпе вверх - плавно сворачиваем
|
|
||||||
if (delta < -10f) {
|
if (delta < -10f) {
|
||||||
isPulledDown = false
|
isPulledDown = false
|
||||||
// 🔥 НЕ сбрасываем overscrollOffset - пусть animatedOverscroll плавно вернётся к 0
|
|
||||||
}
|
}
|
||||||
return Offset(0f, delta) // Consume весь delta
|
return Offset(0f, delta)
|
||||||
}
|
}
|
||||||
val newOffset = (overscrollOffset + delta).coerceAtLeast(0f)
|
val newOffset = (overscrollOffset + delta).coerceAtLeast(0f)
|
||||||
val consumed = overscrollOffset - newOffset
|
val consumed = overscrollOffset - newOffset
|
||||||
overscrollOffset = newOffset
|
overscrollOffset = newOffset
|
||||||
return Offset(0f, -consumed)
|
return Offset(0f, -consumed)
|
||||||
}
|
}
|
||||||
// Затем коллапсируем header
|
|
||||||
if (scrollOffset < maxScrollOffset) {
|
|
||||||
val newScrollOffset = (scrollOffset - delta).coerceIn(0f, maxScrollOffset)
|
|
||||||
val consumed = newScrollOffset - scrollOffset
|
|
||||||
scrollOffset = newScrollOffset
|
|
||||||
return Offset(0f, -consumed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Тянем вниз (delta > 0) - раскрываем header
|
|
||||||
if (delta > 0 && scrollOffset > 0) {
|
|
||||||
val newScrollOffset = (scrollOffset - delta).coerceAtLeast(0f)
|
|
||||||
val consumed = scrollOffset - newScrollOffset
|
|
||||||
scrollOffset = newScrollOffset
|
|
||||||
return Offset(0f, consumed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Offset.Zero
|
return Offset.Zero
|
||||||
@@ -580,9 +589,9 @@ fun ProfileScreen(
|
|||||||
available: Offset,
|
available: Offset,
|
||||||
source: NestedScrollSource
|
source: NestedScrollSource
|
||||||
): Offset {
|
): Offset {
|
||||||
// Overscroll при свайпе вниз от верха
|
// Overscroll при свайпе вниз от верха (когда LazyColumn в начале)
|
||||||
if (available.y > 0 && scrollOffset == 0f) {
|
if (available.y > 0 && scrollOffset == 0f) {
|
||||||
// Telegram: сопротивление если ещё не isPulledDown
|
isDragging = true
|
||||||
val resistance = if (isPulledDown) 1f else 0.5f
|
val resistance = if (isPulledDown) 1f else 0.5f
|
||||||
val delta = available.y * resistance
|
val delta = available.y * resistance
|
||||||
overscrollOffset = (overscrollOffset + delta).coerceAtMost(maxOverscroll)
|
overscrollOffset = (overscrollOffset + delta).coerceAtMost(maxOverscroll)
|
||||||
@@ -591,34 +600,56 @@ fun ProfileScreen(
|
|||||||
return Offset.Zero
|
return Offset.Zero
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════
|
||||||
|
// onPreFling — Telegram ACTION_UP
|
||||||
|
// Вызывается СРАЗУ при отпускании пальца, ДО fling.
|
||||||
|
// Если хедер в промежуточной позиции — перехватываем
|
||||||
|
// velocity и запускаем animateScrollToItem.
|
||||||
|
// LazyColumn НЕ получит velocity → нет fling → нет дёрганья.
|
||||||
|
// ═══════════════════════════════════════════════════════
|
||||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||||
lastVelocity = available.y
|
lastVelocity = available.y
|
||||||
return Velocity.Zero
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
|
||||||
isDragging = false
|
isDragging = false
|
||||||
|
|
||||||
// Telegram: snap логика с учётом velocity
|
// Overscroll snap (аватарка pull-down)
|
||||||
// Если velocity > 1000 и тянем вниз - snap to expanded даже если < 33%
|
|
||||||
// Если velocity < -1000 и тянем вверх - snap to collapsed даже если > 33%
|
|
||||||
val velocityThreshold = 1000f
|
val velocityThreshold = 1000f
|
||||||
|
|
||||||
when {
|
when {
|
||||||
overscrollOffset > snapThreshold || (lastVelocity > velocityThreshold && overscrollOffset > snapThreshold * 0.5f) -> {
|
overscrollOffset > snapThreshold || (lastVelocity > velocityThreshold && overscrollOffset > snapThreshold * 0.5f) -> {
|
||||||
// Snap to expanded
|
|
||||||
isPulledDown = true
|
isPulledDown = true
|
||||||
}
|
}
|
||||||
lastVelocity < -velocityThreshold && overscrollOffset > 0 -> {
|
lastVelocity < -velocityThreshold && overscrollOffset > 0 -> {
|
||||||
// Fast swipe up - snap to collapsed
|
|
||||||
isPulledDown = false
|
isPulledDown = false
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Normal case - snap based on threshold
|
|
||||||
isPulledDown = overscrollOffset > snapThreshold
|
isPulledDown = overscrollOffset > snapThreshold
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HEADER SNAP — как Telegram smoothScrollBy в ACTION_UP
|
||||||
|
val currentOffset = scrollOffset
|
||||||
|
if (currentOffset > 0f && currentOffset < maxScrollOffset) {
|
||||||
|
val progress = currentOffset / maxScrollOffset
|
||||||
|
val snapToCollapsed = when {
|
||||||
|
available.y < -velocityThreshold -> true
|
||||||
|
available.y > velocityThreshold -> false
|
||||||
|
progress >= 0.6f -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
if (snapToCollapsed) {
|
||||||
|
// Snap to collapsed — доскроллить spacer вверх
|
||||||
|
listState.animateScrollToItem(0, maxScrollOffset.toInt())
|
||||||
|
} else {
|
||||||
|
// Snap to expanded — вернуть spacer в начало
|
||||||
|
listState.animateScrollToItem(0, 0)
|
||||||
|
}
|
||||||
|
// Поглощаем velocity — LazyColumn не fling'ит
|
||||||
|
return available
|
||||||
|
}
|
||||||
|
|
||||||
|
return Velocity.Zero
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||||
return Velocity.Zero
|
return Velocity.Zero
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -693,8 +724,18 @@ fun ProfileScreen(
|
|||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
.nestedScroll(nestedScrollConnection)
|
.nestedScroll(nestedScrollConnection)
|
||||||
) {
|
) {
|
||||||
// Scrollable content
|
// Scrollable content — Telegram architecture:
|
||||||
LazyColumn(modifier = Modifier.fillMaxSize().padding(top = headerHeight)) {
|
// Item 0 = spacer (как Telegram RecyclerView item 0)
|
||||||
|
// Скролл LazyColumn двигает КОНТЕНТ + хедер вместе
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
// Item 0: spacer высотой с раскрытый хедер
|
||||||
|
// Когда скроллим вверх — spacer уходит, scrollOffset растёт
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.fillMaxWidth().height(expandedHeaderDp))
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
@@ -902,7 +943,23 @@ fun ProfileScreen(
|
|||||||
},
|
},
|
||||||
hasAvatar = hasAvatar,
|
hasAvatar = hasAvatar,
|
||||||
avatarRepository = avatarRepository,
|
avatarRepository = avatarRepository,
|
||||||
backgroundBlurColorId = backgroundBlurColorId
|
backgroundBlurColorId = backgroundBlurColorId,
|
||||||
|
onAvatarLongPress = {
|
||||||
|
if (hasAvatar) {
|
||||||
|
scope.launch {
|
||||||
|
val avatarList = avatarRepository?.getAvatars(accountPublicKey, allDecode = false)?.first()
|
||||||
|
val first = avatarList?.firstOrNull()
|
||||||
|
if (first != null) {
|
||||||
|
avatarViewerTimestamp = first.timestamp
|
||||||
|
val bmp = withContext(Dispatchers.IO) {
|
||||||
|
AvatarFileManager.base64ToBitmap(first.base64Data)
|
||||||
|
}
|
||||||
|
avatarViewerBitmap = bmp
|
||||||
|
showAvatarViewer = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -918,6 +975,17 @@ fun ProfileScreen(
|
|||||||
},
|
},
|
||||||
isDarkTheme = isDarkTheme
|
isDarkTheme = isDarkTheme
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 🔍 Full-screen avatar viewer
|
||||||
|
FullScreenAvatarViewer(
|
||||||
|
isVisible = showAvatarViewer,
|
||||||
|
onDismiss = { showAvatarViewer = false },
|
||||||
|
displayName = editedName.ifBlank { accountPublicKey.take(10) },
|
||||||
|
avatarTimestamp = avatarViewerTimestamp,
|
||||||
|
avatarBitmap = avatarViewerBitmap,
|
||||||
|
publicKey = accountPublicKey,
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════
|
||||||
@@ -945,7 +1013,8 @@ private fun CollapsingProfileHeader(
|
|||||||
onDeletePhotoClick: () -> Unit,
|
onDeletePhotoClick: () -> Unit,
|
||||||
hasAvatar: Boolean,
|
hasAvatar: Boolean,
|
||||||
avatarRepository: AvatarRepository?,
|
avatarRepository: AvatarRepository?,
|
||||||
backgroundBlurColorId: String = "avatar"
|
backgroundBlurColorId: String = "avatar",
|
||||||
|
onAvatarLongPress: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
@Suppress("UNUSED_VARIABLE")
|
@Suppress("UNUSED_VARIABLE")
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
@@ -987,35 +1056,102 @@ private fun CollapsingProfileHeader(
|
|||||||
val nameFontSize = androidx.compose.ui.unit.lerp(24.sp, 18.sp, collapseProgress)
|
val nameFontSize = androidx.compose.ui.unit.lerp(24.sp, 18.sp, collapseProgress)
|
||||||
val onlineFontSize = androidx.compose.ui.unit.lerp(14.sp, 13.sp, collapseProgress)
|
val onlineFontSize = androidx.compose.ui.unit.lerp(14.sp, 13.sp, collapseProgress)
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxWidth().height(headerHeight)) {
|
Box(modifier = Modifier.fillMaxWidth().height(headerHeight).clipToBounds()) {
|
||||||
// ═══════════════════════════════════════════════════════════
|
// Expansion fraction — computed early so gradient can fade during expansion
|
||||||
// 🎨 BLURRED AVATAR BACKGROUND - всегда показываем
|
val expandFraction = expansionProgress.coerceIn(0f, 1f)
|
||||||
// ═══════════════════════════════════════════════════════════
|
|
||||||
BlurredAvatarBackground(
|
|
||||||
publicKey = publicKey,
|
|
||||||
avatarRepository = avatarRepository,
|
|
||||||
fallbackColor = avatarColors.backgroundColor,
|
|
||||||
blurRadius = 20f,
|
|
||||||
alpha = 0.9f,
|
|
||||||
overlayColors = BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId),
|
|
||||||
isDarkTheme = isDarkTheme
|
|
||||||
)
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// 👤 AVATAR with METABALL EFFECT - Liquid merge animation on scroll
|
// 🎨 BLURRED AVATAR BACKGROUND — ВСЕГДА видим
|
||||||
// При скролле вверх аватарка "сливается" с Dynamic Island
|
// Не fadeout'им при расширении: аватарка растёт поверх blur'а
|
||||||
// Используем metaball эффект для плавного слияния форм
|
// и естественно перекрывает его. Без мерцания.
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
ProfileMetaballEffect(
|
Box(modifier = Modifier.matchParentSize()) {
|
||||||
collapseProgress = collapseProgress,
|
BlurredAvatarBackground(
|
||||||
expansionProgress = expansionProgress,
|
publicKey = publicKey,
|
||||||
statusBarHeight = statusBarHeight,
|
avatarRepository = avatarRepository,
|
||||||
headerHeight = headerHeight,
|
fallbackColor = avatarColors.backgroundColor,
|
||||||
hasAvatar = hasAvatar,
|
blurRadius = 20f,
|
||||||
avatarColor = avatarColors.backgroundColor,
|
alpha = 0.9f,
|
||||||
modifier = Modifier.fillMaxSize()
|
overlayColors = BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId),
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🌅 BOTTOM GRADIENT — плавно исчезает только когда аватарка
|
||||||
|
// уже почти заполнила всю область (90%+)
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
val gradientAlpha = (1f - expandFraction * 1.2f).coerceIn(0f, 1f)
|
||||||
|
if (gradientAlpha > 0.01f) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(80.dp)
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.graphicsLayer { alpha = gradientAlpha }
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
Color.Black.copy(alpha = 0.35f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 👤 AVATAR — Telegram-style expansion to fill header on pull-down
|
||||||
|
// Normal: circle, centered. Pull-down: grows to fill full header width
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
val avatarSize = androidx.compose.ui.unit.lerp(
|
||||||
|
AVATAR_SIZE_EXPANDED, AVATAR_SIZE_COLLAPSED, collapseProgress
|
||||||
|
)
|
||||||
|
val avatarAlpha = (1f - collapseProgress * 1.8f).coerceIn(0f, 1f)
|
||||||
|
val avatarShadowSize = androidx.compose.ui.unit.lerp(12.dp, 0.dp, collapseProgress)
|
||||||
|
val contentAreaHeight = EXPANDED_HEADER_HEIGHT - 70.dp
|
||||||
|
val avatarExpandedY = statusBarHeight + (contentAreaHeight - AVATAR_SIZE_EXPANDED) / 2
|
||||||
|
val avatarCollapsedY = statusBarHeight + (COLLAPSED_HEADER_HEIGHT - AVATAR_SIZE_COLLAPSED) / 2
|
||||||
|
val avatarY = androidx.compose.ui.unit.lerp(avatarExpandedY, avatarCollapsedY, collapseProgress)
|
||||||
|
|
||||||
|
// Telegram-style expansion math
|
||||||
|
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
|
||||||
|
// Avatar width: circle → full screen width
|
||||||
|
val expandedAvatarWidth = androidx.compose.ui.unit.lerp(avatarSize, screenWidth, expandFraction)
|
||||||
|
// Avatar height: circle → full header height
|
||||||
|
val expandedAvatarHeight = androidx.compose.ui.unit.lerp(avatarSize, headerHeight, expandFraction)
|
||||||
|
// Corner radius: fully circular → 0 (square)
|
||||||
|
val cornerRadius = androidx.compose.ui.unit.lerp(avatarSize / 2, 0.dp, expandFraction)
|
||||||
|
// Avatar X: centered → left edge
|
||||||
|
val avatarCenterX = (screenWidth - avatarSize) / 2
|
||||||
|
val expandedAvatarX = androidx.compose.ui.unit.lerp(avatarCenterX, 0.dp, expandFraction)
|
||||||
|
// Avatar Y: normal position → top
|
||||||
|
val expandedAvatarY = androidx.compose.ui.unit.lerp(avatarY, 0.dp, expandFraction)
|
||||||
|
|
||||||
|
// Pre-compute pixel values for graphicsLayer (avoids layout-phase recomposition)
|
||||||
|
val expandedAvatarXPx = with(density) { expandedAvatarX.toPx() }
|
||||||
|
val expandedAvatarYPx = with(density) { expandedAvatarY.toPx() }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(width = expandedAvatarWidth, height = expandedAvatarHeight)
|
||||||
|
.graphicsLayer {
|
||||||
|
translationX = expandedAvatarXPx
|
||||||
|
translationY = expandedAvatarYPx
|
||||||
|
alpha = avatarAlpha
|
||||||
|
shadowElevation = if (expansionProgress > 0.01f) 0f
|
||||||
|
else with(density) { avatarShadowSize.toPx() }
|
||||||
|
shape = RoundedCornerShape(cornerRadius)
|
||||||
|
clip = true
|
||||||
|
}
|
||||||
|
.background(avatarColors.backgroundColor)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onLongPress = { onAvatarLongPress() }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
// Содержимое аватара
|
|
||||||
if (hasAvatar && avatarRepository != null) {
|
if (hasAvatar && avatarRepository != null) {
|
||||||
FullSizeAvatar(
|
FullSizeAvatar(
|
||||||
publicKey = publicKey,
|
publicKey = publicKey,
|
||||||
@@ -1023,23 +1159,51 @@ private fun CollapsingProfileHeader(
|
|||||||
isDarkTheme = isDarkTheme
|
isDarkTheme = isDarkTheme
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Placeholder без аватарки
|
Text(
|
||||||
Box(
|
text = getInitials(name),
|
||||||
modifier = Modifier.fillMaxSize().background(avatarColors.backgroundColor),
|
fontSize = avatarFontSize,
|
||||||
contentAlignment = Alignment.Center
|
fontWeight = FontWeight.Bold,
|
||||||
) {
|
color = avatarColors.textColor
|
||||||
Text(
|
)
|
||||||
text = getInitials(name),
|
|
||||||
fontSize = avatarFontSize,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = avatarColors.textColor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// 🔙 BACK BUTTON
|
// <EFBFBD> ADD/CHANGE AVATAR BUTTON — bottom-right of avatar circle
|
||||||
|
// Fades out on collapse and expansion
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
val cameraButtonAlpha = avatarAlpha * (1f - expandFraction * 4f).coerceIn(0f, 1f)
|
||||||
|
if (cameraButtonAlpha > 0.01f) {
|
||||||
|
val cameraButtonSize = 44.dp
|
||||||
|
// Position: bottom-right of the avatar circle
|
||||||
|
val avatarCenterXPos = screenWidth / 2
|
||||||
|
val avatarCenterYPos = avatarY + avatarSize / 2
|
||||||
|
// Offset to bottom-right edge of circle (45° from center)
|
||||||
|
val offsetFromCenter = avatarSize / 2 * 0.7f // cos(45°) ≈ 0.707
|
||||||
|
val cameraX = avatarCenterXPos + offsetFromCenter - cameraButtonSize / 2
|
||||||
|
val cameraY = avatarCenterYPos + offsetFromCenter - cameraButtonSize / 2
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.offset(x = cameraX, y = cameraY)
|
||||||
|
.size(cameraButtonSize)
|
||||||
|
.graphicsLayer { alpha = cameraButtonAlpha }
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(0xFF3A3A3C))
|
||||||
|
.clickable { onSetPhotoClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = TablerIcons.CameraPlus,
|
||||||
|
contentDescription = "Change avatar",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(22.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// <20>🔙 BACK BUTTON
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -1762,3 +1926,198 @@ fun ProfileNavigationItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
// 🖼️ FULL SCREEN AVATAR VIEWER — Telegram style
|
||||||
|
// Long-press avatar → full screen with swipe-down to dismiss
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
@Composable
|
||||||
|
fun FullScreenAvatarViewer(
|
||||||
|
isVisible: Boolean,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
displayName: String,
|
||||||
|
avatarTimestamp: Long,
|
||||||
|
avatarBitmap: android.graphics.Bitmap?,
|
||||||
|
publicKey: String,
|
||||||
|
isDarkTheme: Boolean
|
||||||
|
) {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val screenHeight = with(density) { LocalConfiguration.current.screenHeightDp.dp.toPx() }
|
||||||
|
|
||||||
|
// Animated visibility
|
||||||
|
var showContent by remember { mutableStateOf(false) }
|
||||||
|
LaunchedEffect(isVisible) {
|
||||||
|
showContent = isVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swipe-to-dismiss offset
|
||||||
|
var dragOffsetY by remember { mutableFloatStateOf(0f) }
|
||||||
|
var isDragging by remember { mutableStateOf(false) }
|
||||||
|
val dismissThreshold = screenHeight * 0.25f
|
||||||
|
|
||||||
|
// Animated values
|
||||||
|
val animatedAlpha by animateFloatAsState(
|
||||||
|
targetValue = if (showContent) {
|
||||||
|
val dragProgress = (kotlin.math.abs(dragOffsetY) / dismissThreshold).coerceIn(0f, 1f)
|
||||||
|
1f - dragProgress * 0.6f
|
||||||
|
} else 0f,
|
||||||
|
animationSpec = tween(if (showContent && !isDragging) 250 else 200),
|
||||||
|
label = "bg_alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
val animatedScale by animateFloatAsState(
|
||||||
|
targetValue = if (showContent) {
|
||||||
|
val dragProgress = (kotlin.math.abs(dragOffsetY) / dismissThreshold).coerceIn(0f, 1f)
|
||||||
|
1f - dragProgress * 0.15f
|
||||||
|
} else 0.8f,
|
||||||
|
animationSpec = tween(if (showContent && !isDragging) 250 else 200),
|
||||||
|
label = "scale"
|
||||||
|
)
|
||||||
|
|
||||||
|
val animatedOffset by animateFloatAsState(
|
||||||
|
targetValue = if (isDragging) dragOffsetY else if (showContent) 0f else 0f,
|
||||||
|
animationSpec = if (isDragging) spring(stiffness = Spring.StiffnessHigh)
|
||||||
|
else spring(dampingRatio = Spring.DampingRatioMediumBouncy),
|
||||||
|
label = "offset",
|
||||||
|
finishedListener = {
|
||||||
|
if (!showContent) onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Date formatting
|
||||||
|
val dateText = remember(avatarTimestamp) {
|
||||||
|
if (avatarTimestamp > 0) {
|
||||||
|
val sdf = SimpleDateFormat("MMMM d, yyyy 'at' h:mm a", Locale.ENGLISH)
|
||||||
|
sdf.format(Date(avatarTimestamp * 1000))
|
||||||
|
} else ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVisible) {
|
||||||
|
BackHandler { showContent = false }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.graphicsLayer { alpha = animatedAlpha }
|
||||||
|
.background(Color.Black)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectVerticalDragGestures(
|
||||||
|
onDragStart = { isDragging = true },
|
||||||
|
onDragEnd = {
|
||||||
|
isDragging = false
|
||||||
|
if (kotlin.math.abs(dragOffsetY) > dismissThreshold) {
|
||||||
|
showContent = false
|
||||||
|
}
|
||||||
|
dragOffsetY = 0f
|
||||||
|
},
|
||||||
|
onDragCancel = {
|
||||||
|
isDragging = false
|
||||||
|
dragOffsetY = 0f
|
||||||
|
},
|
||||||
|
onVerticalDrag = { _, dragAmount ->
|
||||||
|
dragOffsetY += dragAmount
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// Avatar image centered
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.graphicsLayer {
|
||||||
|
translationY = animatedOffset
|
||||||
|
scaleX = animatedScale
|
||||||
|
scaleY = animatedScale
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (avatarBitmap != null) {
|
||||||
|
Image(
|
||||||
|
bitmap = avatarBitmap.asImageBitmap(),
|
||||||
|
contentDescription = "Avatar",
|
||||||
|
modifier = Modifier.fillMaxWidth().aspectRatio(1f),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val avatarColors = getAvatarColor(publicKey, isDarkTheme)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.background(avatarColors.backgroundColor),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = getInitials(displayName.ifBlank { publicKey.take(6) }),
|
||||||
|
fontSize = 80.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = avatarColors.textColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top gradient for readability
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(120.dp)
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Black.copy(alpha = 0.6f),
|
||||||
|
Color.Transparent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Header: back button + name + date
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.statusBarsPadding()
|
||||||
|
.padding(horizontal = 4.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
IconButton(onClick = { showContent = false }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = TablerIcons.ArrowLeft,
|
||||||
|
contentDescription = "Close",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = displayName.ifBlank { publicKey.take(10) },
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = Color.White,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
if (dateText.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = dateText,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
color = Color.White.copy(alpha = 0.7f),
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = { /* menu */ }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = TablerIcons.DotsVertical,
|
||||||
|
contentDescription = "Menu",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ android.useAndroidX=true
|
|||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
|
|
||||||
# Use Java 17 for build
|
# Use Java 17 for build
|
||||||
org.gradle.java.home=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home
|
org.gradle.java.home=/opt/homebrew/opt/openjdk@17
|
||||||
|
|
||||||
# Increase heap size for Gradle
|
# Increase heap size for Gradle
|
||||||
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError --add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
|
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError --add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
|
||||||
|
|||||||
Reference in New Issue
Block a user