feat: Add Palette library for color extraction and enhance avatar handling in ProfileScreen
This commit is contained in:
@@ -82,7 +82,7 @@ dependencies {
|
|||||||
|
|
||||||
// Icons extended
|
// Icons extended
|
||||||
implementation("androidx.compose.material:material-icons-extended:1.5.4")
|
implementation("androidx.compose.material:material-icons-extended:1.5.4")
|
||||||
|
|
||||||
// Tabler Icons for Compose
|
// Tabler Icons for Compose
|
||||||
implementation("br.com.devsrsouza.compose.icons:tabler-icons:1.1.0")
|
implementation("br.com.devsrsouza.compose.icons:tabler-icons:1.1.0")
|
||||||
|
|
||||||
@@ -102,6 +102,9 @@ dependencies {
|
|||||||
// Blurhash for image placeholders
|
// Blurhash for image placeholders
|
||||||
implementation("com.vanniktech:blurhash:0.1.0")
|
implementation("com.vanniktech:blurhash:0.1.0")
|
||||||
|
|
||||||
|
// Palette for extracting colors from images
|
||||||
|
implementation("androidx.palette:palette-ktx:1.0.0")
|
||||||
|
|
||||||
// Crypto libraries for key generation
|
// Crypto libraries for key generation
|
||||||
implementation("org.bitcoinj:bitcoinj-core:0.16.2")
|
implementation("org.bitcoinj:bitcoinj-core:0.16.2")
|
||||||
implementation("org.bouncycastle:bcprov-jdk15to18:1.77")
|
implementation("org.bouncycastle:bcprov-jdk15to18:1.77")
|
||||||
|
|||||||
@@ -452,6 +452,7 @@ fun MessageBubble(
|
|||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.Bottom,
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
AppleEmojiText(
|
AppleEmojiText(
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ 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 androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.palette.graphics.Palette as AndroidPalette
|
||||||
import com.rosetta.messenger.biometric.BiometricAuthManager
|
import com.rosetta.messenger.biometric.BiometricAuthManager
|
||||||
import com.rosetta.messenger.biometric.BiometricAvailability
|
import com.rosetta.messenger.biometric.BiometricAvailability
|
||||||
import com.rosetta.messenger.biometric.BiometricPreferences
|
import com.rosetta.messenger.biometric.BiometricPreferences
|
||||||
@@ -738,6 +739,74 @@ private fun CollapsingProfileHeader(
|
|||||||
// Get actual status bar height
|
// Get actual status bar height
|
||||||
val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🎨 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
|
// 📐 HEADER HEIGHT - ФИКСИРОВАННАЯ! Не меняется при overscroll
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
@@ -831,7 +900,7 @@ private fun CollapsingProfileHeader(
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// 📝 TEXT - внизу header зоны, внутри блока
|
// 📝 TEXT - внизу header зоны, внутри блока
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
val textDefaultY = expandedHeight - 60.dp // Внизу header блока (чуть ниже)
|
val textDefaultY = expandedHeight - 48.dp // Внизу header блока (ближе к низу)
|
||||||
val textCollapsedY = statusBarHeight + COLLAPSED_HEADER_HEIGHT / 2
|
val textCollapsedY = statusBarHeight + COLLAPSED_HEADER_HEIGHT / 2
|
||||||
|
|
||||||
// Текст меняет позицию только при collapse, НЕ при overscroll
|
// Текст меняет позицию только при collapse, НЕ при overscroll
|
||||||
@@ -989,9 +1058,7 @@ private fun CollapsingProfileHeader(
|
|||||||
text = name,
|
text = name,
|
||||||
fontSize = nameFontSize,
|
fontSize = nameFontSize,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color =
|
color = textColor,
|
||||||
if (isColorLight(avatarColors.backgroundColor)) Color.Black
|
|
||||||
else Color.White,
|
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.widthIn(max = 220.dp),
|
modifier = Modifier.widthIn(max = 220.dp),
|
||||||
@@ -1000,7 +1067,7 @@ private fun CollapsingProfileHeader(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
|
||||||
Text(text = "online", fontSize = onlineFontSize, color = Color.White)
|
Text(text = "online", fontSize = onlineFontSize, color = textColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
gradle-warnings.txt
Normal file
6
gradle-warnings.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
> Configure project :app
|
||||||
|
The StartParameter.isConfigurationCacheRequested property has been deprecated. This is scheduled to be removed in Gradle 10.0. Please use 'configurationCache.requested' property on 'BuildFeatures' service instead. Consult the upgrading guide for further information: https://docs.gradle.org/8.14.3/userguide/upgrading_version_8.html#deprecated_startparameter_is_configuration_cache_requested
|
||||||
|
The org.gradle.api.plugins.Convention type has been deprecated. This is scheduled to be removed in Gradle 9.0. Consult the upgrading guide for further information: https://docs.gradle.org/8.14.3/userguide/upgrading_version_8.html#deprecated_access_to_conventions
|
||||||
|
|
||||||
|
[Incubating] Problems report is available at: file:///Users/ruslanmakhmatov/Desktop/Work/rosette-app/rosetta-android/build/reports/problems/problems-report.html
|
||||||
Reference in New Issue
Block a user