feat: add animated badge for top-level requests count in ChatsListScreen
This commit is contained in:
@@ -16,6 +16,7 @@ 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.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
@@ -237,7 +238,11 @@ fun SelectAccountScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create Account Modal
|
// Create Account Modal
|
||||||
if (showCreateModal) {
|
AnimatedVisibility(
|
||||||
|
visible = showCreateModal,
|
||||||
|
enter = fadeIn(animationSpec = tween(200)),
|
||||||
|
exit = fadeOut(animationSpec = tween(150))
|
||||||
|
) {
|
||||||
CreateAccountModal(
|
CreateAccountModal(
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
onCreateNew = onCreateNew,
|
onCreateNew = onCreateNew,
|
||||||
@@ -459,7 +464,17 @@ private fun CreateAccountModal(
|
|||||||
val backgroundColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFFFFFFF)
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||||
|
|
||||||
|
// Анимация карточки: scale + fade с пружинным эффектом
|
||||||
|
val cardScale by animateFloatAsState(
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||||
|
stiffness = Spring.StiffnessMediumLow
|
||||||
|
),
|
||||||
|
label = "cardScale"
|
||||||
|
)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -470,6 +485,10 @@ private fun CreateAccountModal(
|
|||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(32.dp)
|
.padding(32.dp)
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = cardScale
|
||||||
|
scaleY = cardScale
|
||||||
|
}
|
||||||
.clickable(enabled = false, onClick = {}),
|
.clickable(enabled = false, onClick = {}),
|
||||||
shape = RoundedCornerShape(20.dp),
|
shape = RoundedCornerShape(20.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = backgroundColor)
|
colors = CardDefaults.cardColors(containerColor = backgroundColor)
|
||||||
|
|||||||
@@ -494,33 +494,27 @@ fun ChatsListScreen(
|
|||||||
)
|
)
|
||||||
val headerColor = avatarColors.backgroundColor
|
val headerColor = avatarColors.backgroundColor
|
||||||
|
|
||||||
// Header с blur аватарки (fallback = голубой) или акцентным цветом (light)
|
// Header: avatar blur или цвет шапки chat list
|
||||||
Box(modifier = Modifier.fillMaxWidth()) {
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
if (isDarkTheme) {
|
if (backgroundBlurColorId == "avatar") {
|
||||||
if (backgroundBlurColorId == "solid_blue") {
|
// Avatar blur
|
||||||
// Голубой фон
|
BlurredAvatarBackground(
|
||||||
Box(
|
publicKey = accountPublicKey,
|
||||||
modifier = Modifier
|
avatarRepository = avatarRepository,
|
||||||
.matchParentSize()
|
fallbackColor = if (isDarkTheme) Color(0xFF2A2A2A) else PrimaryBlue,
|
||||||
.background(PrimaryBlueDark)
|
blurRadius = 40f,
|
||||||
)
|
alpha = 0.6f,
|
||||||
} else {
|
overlayColors = null,
|
||||||
// Avatar blur (default)
|
isDarkTheme = isDarkTheme
|
||||||
BlurredAvatarBackground(
|
)
|
||||||
publicKey = accountPublicKey,
|
|
||||||
avatarRepository = avatarRepository,
|
|
||||||
fallbackColor = PrimaryBlueDark,
|
|
||||||
blurRadius = 40f,
|
|
||||||
alpha = 0.6f,
|
|
||||||
overlayColors = emptyList(),
|
|
||||||
isDarkTheme = isDarkTheme
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
|
// None или любой другой — стандартный цвет шапки
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.matchParentSize()
|
.matchParentSize()
|
||||||
.background(PrimaryBlue)
|
.background(
|
||||||
|
if (isDarkTheme) Color(0xFF2A2A2A) else PrimaryBlue
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1102,16 +1096,48 @@ fun ChatsListScreen(
|
|||||||
tint =
|
tint =
|
||||||
Color.White
|
Color.White
|
||||||
)
|
)
|
||||||
if (topLevelRequestsCount > 0) {
|
// Badge с числом запросов
|
||||||
|
androidx.compose.animation.AnimatedVisibility(
|
||||||
|
visible = topLevelRequestsCount > 0,
|
||||||
|
enter = scaleIn(
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||||
|
stiffness = Spring.StiffnessMedium
|
||||||
|
)
|
||||||
|
) + fadeIn(),
|
||||||
|
exit = scaleOut() + fadeOut(),
|
||||||
|
modifier = Modifier.align(Alignment.TopEnd)
|
||||||
|
) {
|
||||||
|
val badgeText = remember(topLevelRequestsCount) {
|
||||||
|
when {
|
||||||
|
topLevelRequestsCount > 99 -> "99+"
|
||||||
|
else -> topLevelRequestsCount.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val badgeBg = Color.White
|
||||||
|
val badgeShape = RoundedCornerShape(50)
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier
|
.offset(x = 6.dp, y = (-6).dp)
|
||||||
.align(Alignment.TopEnd)
|
.defaultMinSize(minWidth = 20.dp, minHeight = 20.dp)
|
||||||
.offset(x = 4.dp, y = (-4).dp)
|
.clip(badgeShape)
|
||||||
.size(10.dp)
|
.background(
|
||||||
.clip(CircleShape)
|
color = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4)
|
||||||
.background(if (isDarkTheme) Color.White else PrimaryBlue)
|
)
|
||||||
)
|
.padding(2.dp)
|
||||||
|
.clip(badgeShape)
|
||||||
|
.background(color = badgeBg),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = badgeText,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF0D8CF4),
|
||||||
|
lineHeight = 10.sp,
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3314,11 +3340,13 @@ fun RequestsScreen(
|
|||||||
onPin = { onTogglePin(request.opponentKey) }
|
onPin = { onTogglePin(request.opponentKey) }
|
||||||
)
|
)
|
||||||
|
|
||||||
Divider(
|
if (request != requests.last()) {
|
||||||
modifier = Modifier.padding(start = 84.dp),
|
Divider(
|
||||||
color = dividerColor,
|
modifier = Modifier.padding(start = 84.dp),
|
||||||
thickness = 0.5.dp
|
color = dividerColor,
|
||||||
)
|
thickness = 0.5.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,34 +42,63 @@ fun BoxScope.BlurredAvatarBackground(
|
|||||||
overlayColors: List<Color>? = null,
|
overlayColors: List<Color>? = null,
|
||||||
isDarkTheme: Boolean = true
|
isDarkTheme: Boolean = true
|
||||||
) {
|
) {
|
||||||
// В светлой теме: если дефолтный фон (avatar) — синий как шапка chat list,
|
// В светлой теме с дефолтным фоном (avatar, без overlay) — синий как шапка chat list
|
||||||
// если выбран кастомный цвет в Appearance — используем его
|
if (!isDarkTheme && (overlayColors == null || overlayColors.isEmpty())) {
|
||||||
if (!isDarkTheme) {
|
Box(modifier = Modifier.matchParentSize().background(Color(0xFF0D8CF4)))
|
||||||
val lightBgModifier = if (overlayColors != null && overlayColors.isNotEmpty()) {
|
|
||||||
if (overlayColors.size == 1) {
|
|
||||||
Modifier.matchParentSize().background(overlayColors[0])
|
|
||||||
} else {
|
|
||||||
Modifier.matchParentSize().background(
|
|
||||||
Brush.linearGradient(colors = overlayColors)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Modifier.matchParentSize().background(Color(0xFF0D8CF4))
|
|
||||||
}
|
|
||||||
Box(modifier = lightBgModifier)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если выбран цвет в Appearance — просто сплошной цвет/градиент, без blur
|
// Если выбран цвет в Appearance — рисуем блюр аватарки + полупрозрачный overlay поверх
|
||||||
|
// (одинаково для светлой и тёмной темы, чтобы цвет совпадал с превью в Appearance)
|
||||||
if (overlayColors != null && overlayColors.isNotEmpty()) {
|
if (overlayColors != null && overlayColors.isNotEmpty()) {
|
||||||
val bgModifier = if (overlayColors.size == 1) {
|
// Загружаем блюр аватарки для подложки
|
||||||
Modifier.matchParentSize().background(overlayColors[0])
|
val avatarsForOverlay by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
|
||||||
|
?: remember { mutableStateOf(emptyList()) }
|
||||||
|
val avatarKeyForOverlay = remember(avatarsForOverlay) {
|
||||||
|
avatarsForOverlay.firstOrNull()?.timestamp ?: 0L
|
||||||
|
}
|
||||||
|
var blurredForOverlay by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
LaunchedEffect(avatarKeyForOverlay) {
|
||||||
|
val currentAvatars = avatarsForOverlay
|
||||||
|
if (currentAvatars.isNotEmpty()) {
|
||||||
|
val original = withContext(Dispatchers.IO) {
|
||||||
|
AvatarFileManager.base64ToBitmap(currentAvatars.first().base64Data)
|
||||||
|
}
|
||||||
|
if (original != null) {
|
||||||
|
blurredForOverlay = withContext(Dispatchers.Default) {
|
||||||
|
val scaled = Bitmap.createScaledBitmap(original, original.width / 4, original.height / 4, true)
|
||||||
|
var result = scaled
|
||||||
|
repeat(2) { result = fastBlur(result, (blurRadius / 4).toInt().coerceAtLeast(1)) }
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
blurredForOverlay = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подложка: блюр аватарки или fallback цвет
|
||||||
|
Box(modifier = Modifier.matchParentSize()) {
|
||||||
|
if (blurredForOverlay != null) {
|
||||||
|
Image(
|
||||||
|
bitmap = blurredForOverlay!!.asImageBitmap(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize().graphicsLayer { this.alpha = 0.35f },
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overlay — полупрозрачный, как в Appearance preview
|
||||||
|
val overlayAlpha = if (blurredForOverlay != null) 0.55f else 0.85f
|
||||||
|
val overlayMod = if (overlayColors.size == 1) {
|
||||||
|
Modifier.matchParentSize().background(overlayColors[0].copy(alpha = overlayAlpha))
|
||||||
} else {
|
} else {
|
||||||
Modifier.matchParentSize().background(
|
Modifier.matchParentSize().background(
|
||||||
Brush.linearGradient(colors = overlayColors)
|
Brush.linearGradient(colors = overlayColors.map { it.copy(alpha = overlayAlpha) })
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Box(modifier = bgModifier)
|
Box(modifier = overlayMod)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ object BackgroundBlurPresets {
|
|||||||
label = "Avatar"
|
label = "Avatar"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** Вариант "none" — стандартные цвета шапки без blur и без overlay */
|
||||||
|
val noneDefault = BackgroundBlurOption(
|
||||||
|
id = "none",
|
||||||
|
colors = emptyList(),
|
||||||
|
label = "None"
|
||||||
|
)
|
||||||
|
|
||||||
/** Сплошные цвета */
|
/** Сплошные цвета */
|
||||||
private val solidColors = listOf(
|
private val solidColors = listOf(
|
||||||
BackgroundBlurOption("solid_blue", listOf(Color(0xFF0D8CF4)), "Blue"),
|
BackgroundBlurOption("solid_blue", listOf(Color(0xFF0D8CF4)), "Blue"),
|
||||||
@@ -54,19 +61,19 @@ object BackgroundBlurPresets {
|
|||||||
/** Все варианты в порядке отображения: сначала сплошные, потом градиенты */
|
/** Все варианты в порядке отображения: сначала сплошные, потом градиенты */
|
||||||
val all: List<BackgroundBlurOption> = solidColors + gradients
|
val all: List<BackgroundBlurOption> = solidColors + gradients
|
||||||
|
|
||||||
/** Все варианты включая "Avatar" (default) */
|
/** Все варианты включая "Avatar" и "None" */
|
||||||
val allWithDefault: List<BackgroundBlurOption> = listOf(avatarDefault) + all
|
val allWithDefault: List<BackgroundBlurOption> = listOf(noneDefault, avatarDefault) + all
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Найти вариант по id. Возвращает [avatarDefault] если не найден.
|
* Найти вариант по id. Возвращает [avatarDefault] если не найден.
|
||||||
*/
|
*/
|
||||||
fun findById(id: String): BackgroundBlurOption {
|
fun findById(id: String): BackgroundBlurOption {
|
||||||
return allWithDefault.find { it.id == id } ?: avatarDefault
|
return allWithDefault.find { it.id == id } ?: noneDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получить список цветов для overlay по id.
|
* Получить список цветов для overlay по id.
|
||||||
* Возвращает null если id == "avatar" (значит используется blur аватарки без overlay).
|
* Возвращает null если id == "avatar" или "none" (без overlay).
|
||||||
*/
|
*/
|
||||||
fun getOverlayColors(id: String): List<Color>? {
|
fun getOverlayColors(id: String): List<Color>? {
|
||||||
val option = findById(id)
|
val option = findById(id)
|
||||||
|
|||||||
@@ -252,7 +252,14 @@ private fun ProfileBlurPreview(
|
|||||||
// ═══════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════
|
||||||
// LAYER 2: Color/gradient overlay (или fallback)
|
// LAYER 2: Color/gradient overlay (или fallback)
|
||||||
// ═══════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════
|
||||||
if (overlayColors != null && overlayColors.isNotEmpty()) {
|
if (selectedId == "none") {
|
||||||
|
// None — стандартный цвет шапки
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4))
|
||||||
|
)
|
||||||
|
} else if (overlayColors != null && overlayColors.isNotEmpty()) {
|
||||||
val overlayMod = if (overlayColors.size == 1) {
|
val overlayMod = if (overlayColors.size == 1) {
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -475,6 +482,24 @@ private fun ColorCircleItem(
|
|||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
|
option.id == "none" -> {
|
||||||
|
// None — стандартные цвета, перечеркнутый круг
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4)
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = TablerIcons.X,
|
||||||
|
contentDescription = "None",
|
||||||
|
tint = Color.White.copy(alpha = 0.9f),
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
option.id == "avatar" -> {
|
option.id == "avatar" -> {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -512,8 +537,8 @@ private fun ColorCircleItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Галочка с затемнённым фоном (не для avatar — там уже иконка)
|
// Галочка с затемнённым фоном (не для avatar/none — там уже иконка)
|
||||||
if (isSelected && option.id != "avatar") {
|
if (isSelected && option.id != "avatar" && option.id != "none") {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|||||||
@@ -577,19 +577,21 @@ fun OtherProfileScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// ✉️ WRITE MESSAGE BUTTON
|
// ✉️ WRITE MESSAGE + 📞 CALL BUTTONS
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
Box(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
|
// Write Message
|
||||||
Button(
|
Button(
|
||||||
onClick = { onWriteMessage(user) },
|
onClick = { onWriteMessage(user) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.weight(1f)
|
||||||
.height(48.dp),
|
.height(48.dp),
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
@@ -609,12 +611,43 @@ fun OtherProfileScreen(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Write Message",
|
text = "Message",
|
||||||
fontSize = 16.sp,
|
fontSize = 15.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = Color.White
|
color = Color.White
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Call
|
||||||
|
Button(
|
||||||
|
onClick = { /* TODO: call action */ },
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(48.dp),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFE8E8ED),
|
||||||
|
contentColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
),
|
||||||
|
elevation = ButtonDefaults.buttonElevation(
|
||||||
|
defaultElevation = 0.dp,
|
||||||
|
pressedElevation = 0.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = TablerIcons.Phone,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = if (isDarkTheme) Color.White else PrimaryBlue
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Call",
|
||||||
|
fontSize = 15.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
@@ -662,7 +695,8 @@ fun OtherProfileScreen(
|
|||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = Modifier.fillMaxWidth().heightIn(min = sharedPagerMinHeight),
|
modifier = Modifier.fillMaxWidth().heightIn(min = sharedPagerMinHeight),
|
||||||
beyondBoundsPageCount = 0,
|
beyondBoundsPageCount = 0,
|
||||||
verticalAlignment = Alignment.Top
|
verticalAlignment = Alignment.Top,
|
||||||
|
userScrollEnabled = false
|
||||||
) { page ->
|
) { page ->
|
||||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.TopStart) {
|
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.TopStart) {
|
||||||
OtherProfileSharedTabContent(
|
OtherProfileSharedTabContent(
|
||||||
|
|||||||
@@ -568,6 +568,14 @@ fun ProfileScreen(
|
|||||||
hasTriggeredSnapBackHaptic = true
|
hasTriggeredSnapBackHaptic = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Коллапс при удалении аватара — сразу сворачиваем
|
||||||
|
LaunchedEffect(hasAvatar) {
|
||||||
|
if (!hasAvatar) {
|
||||||
|
isPulledDown = false
|
||||||
|
overscrollOffset = 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// NESTED SCROLL — overscroll (pull-down аватарка) + header snap
|
// NESTED SCROLL — overscroll (pull-down аватарка) + header snap
|
||||||
@@ -605,8 +613,8 @@ fun ProfileScreen(
|
|||||||
available: Offset,
|
available: Offset,
|
||||||
source: NestedScrollSource
|
source: NestedScrollSource
|
||||||
): Offset {
|
): Offset {
|
||||||
// Overscroll при свайпе вниз от верха (когда LazyColumn в начале)
|
// Overscroll при свайпе вниз от верха (только если есть аватар)
|
||||||
if (available.y > 0 && !listState.canScrollBackward) {
|
if (available.y > 0 && !listState.canScrollBackward && hasAvatar) {
|
||||||
isDragging = true
|
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
|
||||||
@@ -1126,15 +1134,24 @@ private fun CollapsingProfileHeader(
|
|||||||
// и естественно перекрывает его. Без мерцания.
|
// и естественно перекрывает его. Без мерцания.
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
Box(modifier = Modifier.matchParentSize()) {
|
Box(modifier = Modifier.matchParentSize()) {
|
||||||
BlurredAvatarBackground(
|
if (backgroundBlurColorId == "none") {
|
||||||
publicKey = publicKey,
|
// None — стандартный цвет шапки без blur
|
||||||
avatarRepository = avatarRepository,
|
Box(
|
||||||
fallbackColor = avatarColors.backgroundColor,
|
modifier = Modifier
|
||||||
blurRadius = 20f,
|
.fillMaxSize()
|
||||||
alpha = 0.9f,
|
.background(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4))
|
||||||
overlayColors = BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId),
|
)
|
||||||
isDarkTheme = isDarkTheme
|
} else {
|
||||||
)
|
BlurredAvatarBackground(
|
||||||
|
publicKey = publicKey,
|
||||||
|
avatarRepository = avatarRepository,
|
||||||
|
fallbackColor = avatarColors.backgroundColor,
|
||||||
|
blurRadius = 20f,
|
||||||
|
alpha = 0.9f,
|
||||||
|
overlayColors = BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId),
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user