feat: Implement avatar management system with P2P delivery
- Added AvatarRepository for handling avatar storage, retrieval, and delivery. - Created AvatarCacheEntity and AvatarDeliveryEntity for database storage. - Introduced PacketAvatar for P2P avatar transfer between clients. - Enhanced RosettaDatabase to include avatar-related tables and migration. - Developed AvatarFileManager for file operations related to avatars. - Implemented AvatarImage composable for displaying user avatars. - Updated ProfileScreen to support avatar selection and updating. - Added functionality for handling incoming avatar packets in ProtocolManager.
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
package com.rosetta.messenger.ui.components
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.util.Base64
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.rosetta.messenger.repository.AvatarRepository
|
||||
import com.rosetta.messenger.ui.chats.AvatarColors
|
||||
import com.rosetta.messenger.ui.chats.getAvatarColor
|
||||
import com.rosetta.messenger.ui.chats.getAvatarText
|
||||
import com.rosetta.messenger.utils.AvatarFileManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Composable для отображения аватара пользователя
|
||||
* Совместимо с desktop версией (AvatarProvider)
|
||||
*
|
||||
* Приоритет отображения:
|
||||
* 1. Реальный аватар из AvatarRepository (если есть)
|
||||
* 2. Цветной placeholder с инициалами (fallback)
|
||||
*
|
||||
* @param publicKey Публичный ключ пользователя
|
||||
* @param avatarRepository Репозиторий аватаров
|
||||
* @param size Размер аватара
|
||||
* @param isDarkTheme Темная тема
|
||||
* @param onClick Обработчик клика (опционально)
|
||||
* @param showOnlineIndicator Показывать индикатор онлайн
|
||||
* @param isOnline Пользователь онлайн
|
||||
*/
|
||||
@Composable
|
||||
fun AvatarImage(
|
||||
publicKey: String,
|
||||
avatarRepository: AvatarRepository?,
|
||||
size: Dp = 40.dp,
|
||||
isDarkTheme: Boolean,
|
||||
onClick: (() -> Unit)? = null,
|
||||
showOnlineIndicator: Boolean = false,
|
||||
isOnline: Boolean = false
|
||||
) {
|
||||
// Получаем аватары из репозитория
|
||||
val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
|
||||
?: remember { mutableStateOf(emptyList()) }
|
||||
|
||||
// Состояние для bitmap
|
||||
var bitmap by remember(avatars) { mutableStateOf<Bitmap?>(null) }
|
||||
|
||||
// Декодируем первый аватар
|
||||
LaunchedEffect(avatars) {
|
||||
bitmap = if (avatars.isNotEmpty()) {
|
||||
withContext(Dispatchers.IO) {
|
||||
AvatarFileManager.base64ToBitmap(avatars.first().base64Data)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(size)
|
||||
.clip(CircleShape)
|
||||
.then(
|
||||
if (onClick != null) {
|
||||
Modifier.clickable(onClick = onClick)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (bitmap != null) {
|
||||
// Отображаем реальный аватар
|
||||
Image(
|
||||
bitmap = bitmap!!.asImageBitmap(),
|
||||
contentDescription = "Avatar",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
} else {
|
||||
// Fallback: цветной placeholder
|
||||
AvatarPlaceholder(
|
||||
publicKey = publicKey,
|
||||
size = size,
|
||||
isDarkTheme = isDarkTheme
|
||||
)
|
||||
}
|
||||
|
||||
// Индикатор онлайн
|
||||
if (showOnlineIndicator && isOnline) {
|
||||
OnlineIndicator(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.size(size / 4)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Цветной placeholder аватара с инициалами
|
||||
* (используется как fallback если нет реального аватара)
|
||||
*/
|
||||
@Composable
|
||||
fun AvatarPlaceholder(
|
||||
publicKey: String,
|
||||
size: Dp = 40.dp,
|
||||
isDarkTheme: Boolean,
|
||||
fontSize: TextUnit? = null
|
||||
) {
|
||||
val avatarColors = getAvatarColor(publicKey, isDarkTheme)
|
||||
val avatarText = getAvatarText(publicKey)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(size)
|
||||
.clip(CircleShape)
|
||||
.background(avatarColors.backgroundColor),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = avatarText,
|
||||
color = avatarColors.textColor,
|
||||
fontSize = fontSize ?: (size.value / 2.5).sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Индикатор онлайн статуса
|
||||
*/
|
||||
@Composable
|
||||
private fun OnlineIndicator(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF4CAF50))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable для выбора аватара (Image Picker)
|
||||
* Использует Android intent для выбора изображения
|
||||
*/
|
||||
@Composable
|
||||
fun AvatarPicker(
|
||||
onAvatarSelected: (String) -> Unit
|
||||
) {
|
||||
// TODO: Реализовать выбор изображения через ActivityResultContract
|
||||
// 1. Использовать rememberLauncherForActivityResult с ActivityResultContracts.GetContent()
|
||||
// 2. Конвертировать URI в ByteArray
|
||||
// 3. Использовать AvatarFileManager.imagePrepareForNetworkTransfer()
|
||||
// 4. Вызвать onAvatarSelected с Base64 PNG
|
||||
}
|
||||
Reference in New Issue
Block a user