feat: Add Palette library for color extraction and enhance avatar handling in ProfileScreen

This commit is contained in:
k1ngsterr1
2026-01-31 05:16:34 +05:00
parent 5fdd30b0ae
commit 430c7d9007
4 changed files with 83 additions and 6 deletions

View File

@@ -452,6 +452,7 @@ fun MessageBubble(
) {
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
AppleEmojiText(

View File

@@ -49,6 +49,7 @@ import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.fragment.app.FragmentActivity
import androidx.palette.graphics.Palette as AndroidPalette
import com.rosetta.messenger.biometric.BiometricAuthManager
import com.rosetta.messenger.biometric.BiometricAvailability
import com.rosetta.messenger.biometric.BiometricPreferences
@@ -738,6 +739,74 @@ private fun CollapsingProfileHeader(
// Get actual status bar height
val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
// ═══════════════════════════════════════════════════════════
// 🎨 DOMINANT COLOR - извлекаем из аватарки для контраста текста
// ═══════════════════════════════════════════════════════════
val avatars by
avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
?: remember { mutableStateOf(emptyList()) }
var avatarBitmap by remember(avatars) { mutableStateOf<android.graphics.Bitmap?>(null) }
var dominantColor by remember { mutableStateOf<Color?>(null) }
Log.d(TAG, "🎨 CollapsingProfileHeader: hasAvatar=$hasAvatar, avatars.size=${avatars.size}, dominantColor=$dominantColor")
LaunchedEffect(avatars, publicKey) {
Log.d(TAG, "🎨 LaunchedEffect avatars: size=${avatars.size}, publicKey=${publicKey.take(8)}")
if (avatars.isNotEmpty()) {
val loadedBitmap = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
AvatarFileManager.base64ToBitmap(avatars.first().base64Data)
}
avatarBitmap = loadedBitmap
Log.d(TAG, "🎨 Bitmap loaded: ${loadedBitmap?.width}x${loadedBitmap?.height}")
// Извлекаем доминантный цвет из нижней части аватарки (где будет текст)
loadedBitmap?.let { bitmap ->
try {
Log.d(TAG, "🎨 Extracting color from bitmap ${bitmap.width}x${bitmap.height}")
// Берем нижнюю треть изображения для более точного определения
val bottomThird =
android.graphics.Bitmap.createBitmap(
bitmap,
0,
(bitmap.height * 2 / 3).coerceAtLeast(1),
bitmap.width,
(bitmap.height / 3).coerceAtLeast(1)
)
Log.d(TAG, "🎨 Bottom third: ${bottomThird.width}x${bottomThird.height}")
val palette = AndroidPalette.from(bottomThird).generate()
Log.d(TAG, "🎨 Palette generated, dominantSwatch=${palette.dominantSwatch}, mutedSwatch=${palette.mutedSwatch}")
// Используем доминантный цвет или muted swatch
val swatch = palette.dominantSwatch ?: palette.mutedSwatch
if (swatch != null) {
val extractedColor = Color(swatch.rgb)
Log.d(TAG, "🎨 Extracted dominant color: R=${extractedColor.red}, G=${extractedColor.green}, B=${extractedColor.blue}, isLight=${isColorLight(extractedColor)}")
dominantColor = extractedColor
} else {
Log.w(TAG, "🎨 No swatch found in palette!")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to extract dominant color: ${e.message}", e)
}
}
} else {
avatarBitmap = null
dominantColor = null
}
}
// Определяем цвет текста на основе фона - derivedStateOf для реактивности
val textColor by remember {
derivedStateOf {
if (hasAvatar && dominantColor != null) {
val isLight = isColorLight(dominantColor!!)
Log.d(TAG, "🎨 Text color: hasAvatar=$hasAvatar, dominantColor=$dominantColor, isLight=$isLight")
if (isLight) Color.Black else Color.White
} else {
if (isColorLight(avatarColors.backgroundColor)) Color.Black else Color.White
}
}
}
// ═══════════════════════════════════════════════════════════
// 📐 HEADER HEIGHT - ФИКСИРОВАННАЯ! Не меняется при overscroll
// ═══════════════════════════════════════════════════════════
@@ -831,7 +900,7 @@ private fun CollapsingProfileHeader(
// ═══════════════════════════════════════════════════════════
// 📝 TEXT - внизу header зоны, внутри блока
// ═══════════════════════════════════════════════════════════
val textDefaultY = expandedHeight - 60.dp // Внизу header блока (чуть ниже)
val textDefaultY = expandedHeight - 48.dp // Внизу header блока (ближе к низу)
val textCollapsedY = statusBarHeight + COLLAPSED_HEADER_HEIGHT / 2
// Текст меняет позицию только при collapse, НЕ при overscroll
@@ -989,9 +1058,7 @@ private fun CollapsingProfileHeader(
text = name,
fontSize = nameFontSize,
fontWeight = FontWeight.SemiBold,
color =
if (isColorLight(avatarColors.backgroundColor)) Color.Black
else Color.White,
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.widthIn(max = 220.dp),
@@ -1000,7 +1067,7 @@ private fun CollapsingProfileHeader(
Spacer(modifier = Modifier.height(2.dp))
Text(text = "online", fontSize = onlineFontSize, color = Color.White)
Text(text = "online", fontSize = onlineFontSize, color = textColor)
}
}
}