feat: Add appearance customization screen with background blur options
- Introduced `BackgroundBlurOption` data class and `BackgroundBlurPresets` object for managing background blur options. - Created `AppearanceScreen` composable for selecting background colors and gradients, including a live preview of the selected option. - Updated `OtherProfileScreen` and `ProfileScreen` to accept and utilize `backgroundBlurColorId` for consistent background blur across profiles. - Enhanced `CollapsingOtherProfileHeader` and `CollapsingProfileHeader` to apply selected background blur options.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
package com.rosetta.messenger.ui.settings
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Модель для варианта фона blur в профиле
|
||||
* @param id Уникальный идентификатор (сохраняется в PreferencesManager)
|
||||
* @param colors Список цветов. Если 1 — сплошной цвет, если 2+ — градиент
|
||||
* @param label Человекочитаемое название
|
||||
*/
|
||||
data class BackgroundBlurOption(
|
||||
val id: String,
|
||||
val colors: List<Color>,
|
||||
val label: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Предустановленные варианты цвета/градиента для background blur в профиле.
|
||||
* Референс: сетка цветных кружков как в desktop-версии.
|
||||
*/
|
||||
object BackgroundBlurPresets {
|
||||
|
||||
/** Вариант "по умолчанию" — используется blur аватарки без цветного overlay */
|
||||
val avatarDefault = BackgroundBlurOption(
|
||||
id = "avatar",
|
||||
colors = emptyList(),
|
||||
label = "Avatar"
|
||||
)
|
||||
|
||||
/** Сплошные цвета */
|
||||
private val solidColors = listOf(
|
||||
BackgroundBlurOption("solid_blue", listOf(Color(0xFF2979FF)), "Blue"),
|
||||
BackgroundBlurOption("solid_green", listOf(Color(0xFF4CAF50)), "Green"),
|
||||
BackgroundBlurOption("solid_orange", listOf(Color(0xFFFF9800)), "Orange"),
|
||||
BackgroundBlurOption("solid_red", listOf(Color(0xFFE53935)), "Red"),
|
||||
BackgroundBlurOption("solid_purple", listOf(Color(0xFF7C4DFF)), "Purple"),
|
||||
BackgroundBlurOption("solid_teal", listOf(Color(0xFF009688)), "Teal"),
|
||||
BackgroundBlurOption("solid_pink", listOf(Color(0xFFE91E63)), "Pink"),
|
||||
BackgroundBlurOption("solid_grey", listOf(Color(0xFF78909C)), "Grey"),
|
||||
)
|
||||
|
||||
/** Градиенты (по 2 цвета, как на референсе) */
|
||||
private val gradients = listOf(
|
||||
BackgroundBlurOption("grad_blue_cyan", listOf(Color(0xFF2979FF), Color(0xFF00BCD4)), "Blue|Cyan"),
|
||||
BackgroundBlurOption("grad_green_lime", listOf(Color(0xFF4CAF50), Color(0xFFCDDC39)), "Green|Lime"),
|
||||
BackgroundBlurOption("grad_orange_yellow", listOf(Color(0xFFFF9800), Color(0xFFFFEB3B)), "Orange|Yellow"),
|
||||
BackgroundBlurOption("grad_red_pink", listOf(Color(0xFFE53935), Color(0xFFFF4081)), "Red|Pink"),
|
||||
BackgroundBlurOption("grad_purple_blue", listOf(Color(0xFF7C4DFF), Color(0xFF536DFE)), "Purple|Blue"),
|
||||
BackgroundBlurOption("grad_teal_green", listOf(Color(0xFF009688), Color(0xFF69F0AE)), "Teal|Green"),
|
||||
BackgroundBlurOption("grad_pink_magenta", listOf(Color(0xFFE91E63), Color(0xFFCE93D8)), "Pink|Magenta"),
|
||||
BackgroundBlurOption("grad_mono", listOf(Color(0xFF546E7A), Color(0xFFB0BEC5)), "Mono"),
|
||||
)
|
||||
|
||||
/** Все варианты в порядке отображения: сначала сплошные, потом градиенты */
|
||||
val all: List<BackgroundBlurOption> = solidColors + gradients
|
||||
|
||||
/** Все варианты включая "Avatar" (default) */
|
||||
val allWithDefault: List<BackgroundBlurOption> = listOf(avatarDefault) + all
|
||||
|
||||
/**
|
||||
* Найти вариант по id. Возвращает [avatarDefault] если не найден.
|
||||
*/
|
||||
fun findById(id: String): BackgroundBlurOption {
|
||||
return allWithDefault.find { it.id == id } ?: avatarDefault
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить список цветов для overlay по id.
|
||||
* Возвращает null если id == "avatar" (значит используется blur аватарки без overlay).
|
||||
*/
|
||||
fun getOverlayColors(id: String): List<Color>? {
|
||||
val option = findById(id)
|
||||
return if (option.colors.isEmpty()) null else option.colors
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,573 @@
|
||||
package com.rosetta.messenger.ui.settings
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import compose.icons.TablerIcons
|
||||
import compose.icons.tablericons.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.rosetta.messenger.repository.AvatarRepository
|
||||
import com.rosetta.messenger.utils.AvatarFileManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import compose.icons.tablericons.Sun
|
||||
import compose.icons.tablericons.Moon
|
||||
|
||||
/**
|
||||
* Экран кастомизации внешнего вида.
|
||||
* Позволяет выбрать цвет/градиент для background blur в профиле.
|
||||
* Preview повторяет avatar block из профиля — реальный blur + аватарка.
|
||||
*/
|
||||
@Composable
|
||||
fun AppearanceScreen(
|
||||
isDarkTheme: Boolean,
|
||||
currentBlurColorId: String,
|
||||
onBack: () -> Unit,
|
||||
onBlurColorChange: (String) -> Unit,
|
||||
onToggleTheme: () -> Unit = {},
|
||||
accountPublicKey: String = "",
|
||||
accountName: String = "",
|
||||
avatarRepository: AvatarRepository? = null
|
||||
) {
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
|
||||
var selectedId by remember { mutableStateOf(currentBlurColorId) }
|
||||
|
||||
BackHandler { onBack() }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(backgroundColor)
|
||||
) {
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// CONTENT
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// LIVE PREVIEW — реальный avatar block из профиля
|
||||
// Покрывает status bar, кнопки overlay поверх
|
||||
// ═══════════════════════════════════════════════════════
|
||||
Box {
|
||||
ProfileBlurPreview(
|
||||
selectedId = selectedId,
|
||||
isDarkTheme = isDarkTheme,
|
||||
publicKey = accountPublicKey,
|
||||
displayName = accountName,
|
||||
avatarRepository = avatarRepository
|
||||
)
|
||||
|
||||
// Overlay: кнопка назад (слева) и смена темы (справа)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.statusBarsPadding()
|
||||
.padding(horizontal = 4.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = TablerIcons.ArrowLeft,
|
||||
contentDescription = "Back",
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onToggleTheme) {
|
||||
Icon(
|
||||
imageVector = if (isDarkTheme) TablerIcons.Sun else TablerIcons.Moon,
|
||||
contentDescription = "Toggle theme",
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// SECTION TITLE
|
||||
// ═══════════════════════════════════════════════════════
|
||||
Text(
|
||||
text = "BACKGROUND COLOR",
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = secondaryTextColor,
|
||||
letterSpacing = 0.5.sp,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// COLOR GRID
|
||||
// ═══════════════════════════════════════════════════════
|
||||
ColorSelectionGrid(
|
||||
selectedId = selectedId,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onSelect = { id ->
|
||||
selectedId = id
|
||||
onBlurColorChange(id)
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Choose a color or gradient overlay for the blurred header background in your profile.",
|
||||
fontSize = 13.sp,
|
||||
color = secondaryTextColor,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
lineHeight = 18.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// 🎨 LIVE PREVIEW — повторяет avatar block из ProfileScreen
|
||||
// Реальный blur аватарки + overlay + круглая аватарка + имя
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Composable
|
||||
private fun ProfileBlurPreview(
|
||||
selectedId: String,
|
||||
isDarkTheme: Boolean,
|
||||
publicKey: String,
|
||||
displayName: String,
|
||||
avatarRepository: AvatarRepository?
|
||||
) {
|
||||
val option = BackgroundBlurPresets.findById(selectedId)
|
||||
val overlayColors = BackgroundBlurPresets.getOverlayColors(selectedId)
|
||||
val avatarColors = getAvatarColor(publicKey, isDarkTheme)
|
||||
|
||||
// Загрузка аватарки
|
||||
val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
|
||||
?: remember { mutableStateOf(emptyList()) }
|
||||
|
||||
val avatarKey = remember(avatars) { avatars.firstOrNull()?.timestamp ?: 0L }
|
||||
var avatarBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||
var blurredBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||
|
||||
LaunchedEffect(avatarKey) {
|
||||
val current = avatars
|
||||
if (current.isNotEmpty()) {
|
||||
val decoded = withContext(Dispatchers.IO) {
|
||||
AvatarFileManager.base64ToBitmap(current.first().base64Data)
|
||||
}
|
||||
if (decoded != null) {
|
||||
avatarBitmap = decoded
|
||||
// Blur для фонового изображения
|
||||
blurredBitmap = withContext(Dispatchers.Default) {
|
||||
val scaled = Bitmap.createScaledBitmap(
|
||||
decoded,
|
||||
decoded.width / 4,
|
||||
decoded.height / 4,
|
||||
true
|
||||
)
|
||||
var result = scaled
|
||||
repeat(3) {
|
||||
result = fastBlur(result, 6)
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
} else {
|
||||
avatarBitmap = null
|
||||
blurredBitmap = null
|
||||
}
|
||||
}
|
||||
|
||||
// Анимированный label
|
||||
val labelText = if (option.colors.isEmpty()) "Avatar Blur" else option.label
|
||||
|
||||
// Preview — повторяет profile header, покрывает status bar
|
||||
val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(280.dp + statusBarHeight)
|
||||
) {
|
||||
// ═══════════════════════════════════════════════════
|
||||
// LAYER 1: Blurred avatar background (как в профиле)
|
||||
// ═══════════════════════════════════════════════════
|
||||
if (blurredBitmap != null) {
|
||||
Image(
|
||||
bitmap = blurredBitmap!!.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer { alpha = 0.35f },
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// LAYER 2: Color/gradient overlay (или fallback)
|
||||
// ═══════════════════════════════════════════════════
|
||||
if (overlayColors != null && overlayColors.isNotEmpty()) {
|
||||
val overlayMod = if (overlayColors.size == 1) {
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(overlayColors[0].copy(alpha = if (blurredBitmap != null) 0.55f else 0.85f))
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
colors = overlayColors.map {
|
||||
it.copy(alpha = if (blurredBitmap != null) 0.55f else 0.85f)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
Box(modifier = overlayMod)
|
||||
} else if (blurredBitmap != null) {
|
||||
// Стандартный затемняющий overlay (как в профиле)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(avatarColors.backgroundColor.copy(alpha = 0.3f))
|
||||
)
|
||||
} else {
|
||||
// Нет аватарки и нет overlay — fallback цвет
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
if (isDarkTheme) Color(0xFF2A2A2E) else Color(0xFFD8D8DC)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// LAYER 3: Тонкий нижний градиент-затемнение
|
||||
// ═══════════════════════════════════════════════════
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(80.dp)
|
||||
.align(Alignment.BottomCenter)
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
Color.Black.copy(alpha = 0.35f)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// LAYER 4: Avatar circle + Name + subtitle
|
||||
// Повторяет layout из CollapsingProfileHeader
|
||||
// ═══════════════════════════════════════════════════
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = statusBarHeight)
|
||||
.padding(bottom = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
// Круглая аватарка с shadow — 120dp как в ProfileScreen
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(120.dp)
|
||||
.shadow(12.dp, CircleShape)
|
||||
.clip(CircleShape)
|
||||
.background(avatarColors.backgroundColor),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (avatarBitmap != null) {
|
||||
Image(
|
||||
bitmap = avatarBitmap!!.asImageBitmap(),
|
||||
contentDescription = "Avatar",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
} else {
|
||||
// Placeholder с инициалами (40sp как в ProfileScreen)
|
||||
Text(
|
||||
text = getInitials(displayName.ifBlank { publicKey.take(6) }),
|
||||
fontSize = 40.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = avatarColors.textColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(14.dp))
|
||||
|
||||
// Имя пользователя
|
||||
Text(
|
||||
text = displayName.ifBlank { publicKey.take(10) + "..." },
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
// Label текущего пресета — с иконкой стрелки для градиентов
|
||||
val labelParts = labelText.split("|")
|
||||
if (labelParts.size == 2) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = labelParts[0],
|
||||
fontSize = 14.sp,
|
||||
color = Color.White.copy(alpha = 0.7f)
|
||||
)
|
||||
Icon(
|
||||
imageVector = TablerIcons.ArrowNarrowRight,
|
||||
contentDescription = null,
|
||||
tint = Color.White.copy(alpha = 0.5f),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = labelParts[1],
|
||||
fontSize = 14.sp,
|
||||
color = Color.White.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = labelText,
|
||||
fontSize = 14.sp,
|
||||
color = Color.White.copy(alpha = 0.7f),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// 🎨 COLOR GRID — сетка выбора цветов (8 в ряду)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@Composable
|
||||
private fun ColorSelectionGrid(
|
||||
selectedId: String,
|
||||
isDarkTheme: Boolean,
|
||||
onSelect: (String) -> Unit
|
||||
) {
|
||||
val allOptions = BackgroundBlurPresets.allWithDefault
|
||||
val columns = 8
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp)
|
||||
) {
|
||||
allOptions.chunked(columns).forEach { rowItems ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
rowItems.forEach { option ->
|
||||
ColorCircleItem(
|
||||
option = option,
|
||||
isSelected = option.id == selectedId,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onClick = { onSelect(option.id) }
|
||||
)
|
||||
}
|
||||
repeat(columns - rowItems.size) {
|
||||
Spacer(modifier = Modifier.size(40.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColorCircleItem(
|
||||
option: BackgroundBlurOption,
|
||||
isSelected: Boolean,
|
||||
isDarkTheme: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (isSelected) 1.15f else 1.0f,
|
||||
animationSpec = tween(200),
|
||||
label = "scale"
|
||||
)
|
||||
|
||||
val borderColor by animateColorAsState(
|
||||
targetValue = if (isSelected) {
|
||||
if (isDarkTheme) Color.White else Color(0xFF222222)
|
||||
} else {
|
||||
Color.Transparent
|
||||
},
|
||||
animationSpec = tween(200),
|
||||
label = "border"
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.scale(scale)
|
||||
.clip(CircleShape)
|
||||
.border(
|
||||
width = if (isSelected) 2.5.dp else 0.5.dp,
|
||||
color = if (isSelected) borderColor else Color.White.copy(alpha = 0.12f),
|
||||
shape = CircleShape
|
||||
)
|
||||
.clickable(onClick = onClick),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
when {
|
||||
option.id == "avatar" -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
colors = listOf(Color(0xFF3A3A3C), Color(0xFF8E8E93))
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = TablerIcons.CircleOff,
|
||||
contentDescription = "Default",
|
||||
tint = Color.White.copy(alpha = 0.9f),
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
option.colors.size == 1 -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(option.colors[0])
|
||||
)
|
||||
}
|
||||
option.colors.size >= 2 -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.linearGradient(colors = option.colors)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Галочка с затемнённым фоном
|
||||
if (isSelected) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.25f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = TablerIcons.Check,
|
||||
contentDescription = "Selected",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Быстрый box blur (для preview, идентично BlurredAvatarBackground)
|
||||
*/
|
||||
private fun fastBlur(source: Bitmap, radius: Int): Bitmap {
|
||||
if (radius < 1) return source
|
||||
val w = source.width
|
||||
val h = source.height
|
||||
val bitmap = source.copy(source.config, true)
|
||||
val pixels = IntArray(w * h)
|
||||
bitmap.getPixels(pixels, 0, w, 0, 0, w, h)
|
||||
for (y in 0 until h) blurRow(pixels, y, w, radius)
|
||||
for (x in 0 until w) blurColumn(pixels, x, w, h, radius)
|
||||
bitmap.setPixels(pixels, 0, w, 0, 0, w, h)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
private fun blurRow(pixels: IntArray, y: Int, w: Int, radius: Int) {
|
||||
var sR = 0; var sG = 0; var sB = 0; var sA = 0
|
||||
val dv = radius * 2 + 1; val off = y * w
|
||||
for (i in -radius..radius) {
|
||||
val x = i.coerceIn(0, w - 1); val p = pixels[off + x]
|
||||
sA += (p shr 24) and 0xff; sR += (p shr 16) and 0xff
|
||||
sG += (p shr 8) and 0xff; sB += p and 0xff
|
||||
}
|
||||
for (x in 0 until w) {
|
||||
pixels[off + x] = ((sA / dv) shl 24) or ((sR / dv) shl 16) or ((sG / dv) shl 8) or (sB / dv)
|
||||
val xL = (x - radius).coerceIn(0, w - 1); val xR = (x + radius + 1).coerceIn(0, w - 1)
|
||||
val lp = pixels[off + xL]; val rp = pixels[off + xR]
|
||||
sA += ((rp shr 24) and 0xff) - ((lp shr 24) and 0xff)
|
||||
sR += ((rp shr 16) and 0xff) - ((lp shr 16) and 0xff)
|
||||
sG += ((rp shr 8) and 0xff) - ((lp shr 8) and 0xff)
|
||||
sB += (rp and 0xff) - (lp and 0xff)
|
||||
}
|
||||
}
|
||||
|
||||
private fun blurColumn(pixels: IntArray, x: Int, w: Int, h: Int, radius: Int) {
|
||||
var sR = 0; var sG = 0; var sB = 0; var sA = 0
|
||||
val dv = radius * 2 + 1
|
||||
for (i in -radius..radius) {
|
||||
val y = i.coerceIn(0, h - 1); val p = pixels[y * w + x]
|
||||
sA += (p shr 24) and 0xff; sR += (p shr 16) and 0xff
|
||||
sG += (p shr 8) and 0xff; sB += p and 0xff
|
||||
}
|
||||
for (y in 0 until h) {
|
||||
val off = y * w + x
|
||||
pixels[off] = ((sA / dv) shl 24) or ((sR / dv) shl 16) or ((sG / dv) shl 8) or (sB / dv)
|
||||
val yT = (y - radius).coerceIn(0, h - 1); val yB = (y + radius + 1).coerceIn(0, h - 1)
|
||||
val tp = pixels[yT * w + x]; val bp = pixels[yB * w + x]
|
||||
sA += ((bp shr 24) and 0xff) - ((tp shr 24) and 0xff)
|
||||
sR += ((bp shr 16) and 0xff) - ((tp shr 16) and 0xff)
|
||||
sG += ((bp shr 8) and 0xff) - ((tp shr 8) and 0xff)
|
||||
sB += (bp and 0xff) - (tp and 0xff)
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,8 @@ fun OtherProfileScreen(
|
||||
user: SearchUser,
|
||||
isDarkTheme: Boolean,
|
||||
onBack: () -> Unit,
|
||||
avatarRepository: AvatarRepository? = null
|
||||
avatarRepository: AvatarRepository? = null,
|
||||
backgroundBlurColorId: String = "avatar"
|
||||
) {
|
||||
var isBlocked by remember { mutableStateOf(false) }
|
||||
var showAvatarMenu by remember { mutableStateOf(false) }
|
||||
@@ -457,7 +458,8 @@ fun OtherProfileScreen(
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
backgroundBlurColorId = backgroundBlurColorId
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -483,7 +485,8 @@ private fun CollapsingOtherProfileHeader(
|
||||
isBlocked: Boolean,
|
||||
onBlockToggle: () -> Unit,
|
||||
avatarRepository: AvatarRepository? = null,
|
||||
onClearChat: () -> Unit
|
||||
onClearChat: () -> Unit,
|
||||
backgroundBlurColorId: String = "avatar"
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
|
||||
@@ -521,7 +524,8 @@ private fun CollapsingOtherProfileHeader(
|
||||
avatarRepository = avatarRepository,
|
||||
fallbackColor = avatarColors.backgroundColor,
|
||||
blurRadius = 25f,
|
||||
alpha = 0.3f
|
||||
alpha = 0.3f,
|
||||
overlayColors = com.rosetta.messenger.ui.settings.BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId)
|
||||
)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
@@ -189,13 +189,15 @@ fun ProfileScreen(
|
||||
onSaveProfile: (name: String, username: String) -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
onNavigateToTheme: () -> Unit = {},
|
||||
onNavigateToAppearance: () -> Unit = {},
|
||||
onNavigateToSafety: () -> Unit = {},
|
||||
onNavigateToLogs: () -> Unit = {},
|
||||
onNavigateToCrashLogs: () -> Unit = {},
|
||||
onNavigateToBiometric: () -> Unit = {},
|
||||
viewModel: ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel(),
|
||||
avatarRepository: AvatarRepository? = null,
|
||||
dialogDao: com.rosetta.messenger.database.DialogDao? = null
|
||||
dialogDao: com.rosetta.messenger.database.DialogDao? = null,
|
||||
backgroundBlurColorId: String = "avatar"
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val activity = context as? FragmentActivity
|
||||
@@ -633,6 +635,14 @@ fun ProfileScreen(
|
||||
showDivider = true
|
||||
)
|
||||
|
||||
TelegramSettingsItem(
|
||||
icon = TablerIcons.Brush,
|
||||
title = "Appearance",
|
||||
onClick = onNavigateToAppearance,
|
||||
isDarkTheme = isDarkTheme,
|
||||
showDivider = true
|
||||
)
|
||||
|
||||
TelegramSettingsItem(
|
||||
icon = TablerIcons.Lock,
|
||||
title = "Safety",
|
||||
@@ -716,7 +726,8 @@ fun ProfileScreen(
|
||||
}
|
||||
},
|
||||
hasAvatar = hasAvatar,
|
||||
avatarRepository = avatarRepository
|
||||
avatarRepository = avatarRepository,
|
||||
backgroundBlurColorId = backgroundBlurColorId
|
||||
)
|
||||
}
|
||||
|
||||
@@ -757,7 +768,8 @@ private fun CollapsingProfileHeader(
|
||||
onSetPhotoClick: () -> Unit,
|
||||
onDeletePhotoClick: () -> Unit,
|
||||
hasAvatar: Boolean,
|
||||
avatarRepository: AvatarRepository?
|
||||
avatarRepository: AvatarRepository?,
|
||||
backgroundBlurColorId: String = "avatar"
|
||||
) {
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val density = LocalDensity.current
|
||||
@@ -808,7 +820,8 @@ private fun CollapsingProfileHeader(
|
||||
avatarRepository = avatarRepository,
|
||||
fallbackColor = avatarColors.backgroundColor,
|
||||
blurRadius = 25f,
|
||||
alpha = 0.3f
|
||||
alpha = 0.3f,
|
||||
overlayColors = BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId)
|
||||
)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user