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:
@@ -3,7 +3,10 @@ package com.rosetta.messenger.ui.settings
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.*
|
||||
@@ -47,9 +50,14 @@ import androidx.fragment.app.FragmentActivity
|
||||
import com.rosetta.messenger.biometric.BiometricAuthManager
|
||||
import com.rosetta.messenger.biometric.BiometricAvailability
|
||||
import com.rosetta.messenger.biometric.BiometricPreferences
|
||||
import com.rosetta.messenger.repository.AvatarRepository
|
||||
import com.rosetta.messenger.utils.AvatarFileManager
|
||||
import com.rosetta.messenger.ui.components.AvatarImage
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val TAG = "ProfileScreen"
|
||||
@@ -135,7 +143,8 @@ fun ProfileScreen(
|
||||
onNavigateToTheme: () -> Unit = {},
|
||||
onNavigateToSafety: () -> Unit = {},
|
||||
onNavigateToLogs: () -> Unit = {},
|
||||
viewModel: ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
|
||||
viewModel: ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel(),
|
||||
avatarRepository: AvatarRepository? = null
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val activity = context as? FragmentActivity
|
||||
@@ -156,6 +165,46 @@ fun ProfileScreen(
|
||||
// Состояние меню аватара для установки фото профиля
|
||||
var showAvatarMenu by remember { mutableStateOf(false) }
|
||||
|
||||
// Image picker launcher для выбора аватара
|
||||
val imagePickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.GetContent()
|
||||
) { uri: Uri? ->
|
||||
uri?.let {
|
||||
scope.launch {
|
||||
try {
|
||||
// Читаем файл изображения
|
||||
val inputStream = context.contentResolver.openInputStream(uri)
|
||||
val imageBytes = inputStream?.readBytes()
|
||||
inputStream?.close()
|
||||
|
||||
if (imageBytes != null) {
|
||||
// Конвертируем в PNG Base64 (кросс-платформенная совместимость)
|
||||
val base64Png = withContext(Dispatchers.IO) {
|
||||
AvatarFileManager.imagePrepareForNetworkTransfer(context, imageBytes)
|
||||
}
|
||||
|
||||
// Сохраняем аватар через репозиторий
|
||||
avatarRepository?.changeMyAvatar(base64Png)
|
||||
|
||||
// Показываем успешное сообщение
|
||||
android.widget.Toast.makeText(
|
||||
context,
|
||||
"Avatar updated successfully",
|
||||
android.widget.Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to upload avatar", e)
|
||||
android.widget.Toast.makeText(
|
||||
context,
|
||||
"Failed to update avatar: ${e.message}",
|
||||
android.widget.Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Цвета в зависимости от темы
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
|
||||
val surfaceColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
|
||||
@@ -402,7 +451,11 @@ fun ProfileScreen(
|
||||
},
|
||||
isDarkTheme = isDarkTheme,
|
||||
showAvatarMenu = showAvatarMenu,
|
||||
onAvatarMenuChange = { showAvatarMenu = it }
|
||||
onAvatarMenuChange = { showAvatarMenu = it },
|
||||
onSetPhotoClick = {
|
||||
imagePickerLauncher.launch("image/*")
|
||||
},
|
||||
avatarRepository = avatarRepository
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -422,7 +475,9 @@ private fun CollapsingProfileHeader(
|
||||
onSave: () -> Unit,
|
||||
isDarkTheme: Boolean,
|
||||
showAvatarMenu: Boolean,
|
||||
onAvatarMenuChange: (Boolean) -> Unit
|
||||
onAvatarMenuChange: (Boolean) -> Unit,
|
||||
onSetPhotoClick: () -> Unit,
|
||||
avatarRepository: AvatarRepository?
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val configuration = LocalConfiguration.current
|
||||
@@ -522,13 +577,13 @@ private fun CollapsingProfileHeader(
|
||||
isDarkTheme = isDarkTheme,
|
||||
onSetPhotoClick = {
|
||||
onAvatarMenuChange(false)
|
||||
// TODO: Реализовать выбор фото профиля
|
||||
onSetPhotoClick()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 👤 AVATAR - shrinks and moves up
|
||||
// 👤 AVATAR - shrinks and moves up (with real avatar support)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
if (avatarSize > 1.dp) {
|
||||
Box(
|
||||
@@ -541,17 +596,36 @@ private fun CollapsingProfileHeader(
|
||||
.clip(CircleShape)
|
||||
.background(Color.White.copy(alpha = 0.15f))
|
||||
.padding(2.dp)
|
||||
.clip(CircleShape)
|
||||
.background(avatarColors.backgroundColor),
|
||||
.clip(CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (avatarFontSize > 1.sp) {
|
||||
Text(
|
||||
text = getInitials(name),
|
||||
fontSize = avatarFontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = avatarColors.textColor
|
||||
// Используем AvatarImage если репозиторий доступен
|
||||
if (avatarRepository != null) {
|
||||
AvatarImage(
|
||||
publicKey = publicKey,
|
||||
avatarRepository = avatarRepository,
|
||||
size = avatarSize - 4.dp,
|
||||
isDarkTheme = false, // Header всегда светлый на цветном фоне
|
||||
onClick = null,
|
||||
showOnlineIndicator = false
|
||||
)
|
||||
} else {
|
||||
// Fallback: цветной placeholder с инициалами
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(avatarColors.backgroundColor),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (avatarFontSize > 1.sp) {
|
||||
Text(
|
||||
text = getInitials(name),
|
||||
fontSize = avatarFontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = avatarColors.textColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user