diff --git a/app/src/main/java/com/rosetta/messenger/database/AvatarEntities.kt b/app/src/main/java/com/rosetta/messenger/database/AvatarEntities.kt index cb998d6..745e9f9 100644 --- a/app/src/main/java/com/rosetta/messenger/database/AvatarEntities.kt +++ b/app/src/main/java/com/rosetta/messenger/database/AvatarEntities.kt @@ -67,6 +67,18 @@ interface AvatarDao { @Query("DELETE FROM avatar_cache WHERE public_key = :publicKey") suspend fun deleteAvatars(publicKey: String) + /** + * Удалить все аватары пользователя (alias для deleteAvatars) + */ + @Query("DELETE FROM avatar_cache WHERE public_key = :publicKey") + suspend fun deleteAllAvatars(publicKey: String) + + /** + * Получить все аватары пользователя (не Flow, для удаления файлов) + */ + @Query("SELECT * FROM avatar_cache WHERE public_key = :publicKey") + suspend fun getAvatarsByPublicKey(publicKey: String): List + /** * Удалить старые аватары (оставить только N последних) */ diff --git a/app/src/main/java/com/rosetta/messenger/repository/AvatarRepository.kt b/app/src/main/java/com/rosetta/messenger/repository/AvatarRepository.kt index ed84a65..d73dbfa 100644 --- a/app/src/main/java/com/rosetta/messenger/repository/AvatarRepository.kt +++ b/app/src/main/java/com/rosetta/messenger/repository/AvatarRepository.kt @@ -167,6 +167,43 @@ class AvatarRepository( } } + /** + * Удалить свой аватар + */ + suspend fun deleteMyAvatar() { + withContext(Dispatchers.IO) { + try { + Log.d(TAG, "🗑️ deleteMyAvatar called") + Log.d(TAG, "👤 Current public key: ${currentPublicKey.take(16)}...") + + // Получаем все аватары пользователя + val avatars = avatarDao.getAvatarsByPublicKey(currentPublicKey) + + // Удаляем файлы + for (avatar in avatars) { + try { + AvatarFileManager.deleteAvatar(context, avatar.avatar) + Log.d(TAG, "✅ Deleted avatar file: ${avatar.avatar}") + } catch (e: Exception) { + Log.w(TAG, "⚠️ Failed to delete avatar file: ${avatar.avatar}", e) + } + } + + // Удаляем из БД + avatarDao.deleteAllAvatars(currentPublicKey) + Log.d(TAG, "✅ Avatars deleted from DB") + + // Очищаем memory cache + memoryCache.remove(currentPublicKey) + + Log.d(TAG, "🎉 Avatar deleted successfully!") + } catch (e: Exception) { + Log.e(TAG, "❌ Failed to delete avatar: ${e.message}", e) + throw e + } + } + } + /** * Загрузить и расшифровать аватар из файла */ diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt index 89cfb05..7bbb6fa 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt @@ -1137,8 +1137,12 @@ fun ProfilePhotoMenu( expanded: Boolean, onDismiss: () -> Unit, isDarkTheme: Boolean, - onSetPhotoClick: () -> Unit + onSetPhotoClick: () -> Unit, + onDeletePhotoClick: (() -> Unit)? = null, + hasAvatar: Boolean = false ) { + val dividerColor = if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.08f) + DropdownMenu( expanded = expanded, onDismissRequest = onDismiss, @@ -1156,6 +1160,25 @@ fun ProfilePhotoMenu( tintColor = if (isDarkTheme) Color.White else Color.Black, textColor = if (isDarkTheme) Color.White else Color.Black ) + + // Показываем Delete только если есть аватар + if (hasAvatar && onDeletePhotoClick != null) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + .height(0.5.dp) + .background(dividerColor) + ) + + ProfilePhotoMenuItem( + icon = TablerIcons.Trash, + text = "Delete Photo", + onClick = onDeletePhotoClick, + tintColor = Color(0xFFFF3B30), + textColor = Color(0xFFFF3B30) + ) + } } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt index 9ccaee2..4f995cb 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt @@ -267,6 +267,11 @@ fun ProfileScreen( val textColor = if (isDarkTheme) Color.White else Color.Black val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) val avatarColors = getAvatarColor(accountPublicKey, isDarkTheme) + + // Проверяем наличие аватара + val avatars by avatarRepository?.getAvatars(accountPublicKey, allDecode = false)?.collectAsState() + ?: remember { mutableStateOf(emptyList()) } + val hasAvatar = avatars.isNotEmpty() // State for editing - Update when account data changes var editedName by remember(accountName) { mutableStateOf(accountName) } @@ -520,6 +525,18 @@ fun ProfileScreen( onSetPhotoClick = { imagePickerLauncher.launch("image/*") }, + onDeletePhotoClick = { + // Удаляем аватар + scope.launch { + avatarRepository?.deleteMyAvatar() + android.widget.Toast.makeText( + context, + "Avatar deleted", + android.widget.Toast.LENGTH_SHORT + ).show() + } + }, + hasAvatar = hasAvatar, avatarRepository = avatarRepository ) } @@ -636,6 +653,8 @@ private fun CollapsingProfileHeader( showAvatarMenu: Boolean, onAvatarMenuChange: (Boolean) -> Unit, onSetPhotoClick: () -> Unit, + onDeletePhotoClick: () -> Unit, + hasAvatar: Boolean, avatarRepository: AvatarRepository? ) { val density = LocalDensity.current @@ -654,35 +673,57 @@ private fun CollapsingProfileHeader( val headerHeight = androidx.compose.ui.unit.lerp(expandedHeight, collapsedHeight, collapseProgress) // ═══════════════════════════════════════════════════════════ - // 👤 AVATAR - По умолчанию квадратный во весь экран, при скролле становится круглым + // 👤 AVATAR - По умолчанию прямоугольный во весь header, при скролле становится круглым // ═══════════════════════════════════════════════════════════ - // Размер: ширина = screenWidthDp (полная ширина), высота = expandedHeight - // При скролле уменьшается до 0 - val avatarWidth = androidx.compose.ui.unit.lerp(screenWidthDp, 0.dp, collapseProgress) - val avatarHeight = androidx.compose.ui.unit.lerp(expandedHeight, 0.dp, collapseProgress) + // Размеры аватара - всегда помещается в header + // При collapseProgress=0: ширина=screenWidth, высота=expandedHeight + // При collapseProgress=1: размер=0 - // Для cornerRadius и других расчётов используем меньшую сторону + // Промежуточный размер для круга (когда становится круглым) + val circleSize = AVATAR_SIZE_EXPANDED + + // Плавный переход: сначала уменьшается до круга, потом до 0 + val avatarWidth: Dp + val avatarHeight: Dp + + if (collapseProgress < 0.4f) { + // Фаза 1: от полного размера до круга + val phase1Progress = collapseProgress / 0.4f + avatarWidth = androidx.compose.ui.unit.lerp(screenWidthDp, circleSize, phase1Progress) + avatarHeight = androidx.compose.ui.unit.lerp(expandedHeight, circleSize, phase1Progress) + } else { + // Фаза 2: от круга до 0 + val phase2Progress = (collapseProgress - 0.4f) / 0.6f + avatarWidth = androidx.compose.ui.unit.lerp(circleSize, 0.dp, phase2Progress) + avatarHeight = androidx.compose.ui.unit.lerp(circleSize, 0.dp, phase2Progress) + } + + // Для cornerRadius используем меньшую сторону val avatarSize = minOf(avatarWidth, avatarHeight) - // Позиция X: от 0 (весь экран) до центра - val avatarCenterX = (screenWidthDp - AVATAR_SIZE_EXPANDED) / 2 - val avatarX = androidx.compose.ui.unit.lerp(0.dp, avatarCenterX + (AVATAR_SIZE_EXPANDED - avatarSize) / 2, collapseProgress.coerceIn(0f, 0.5f) * 2f) + // Позиция X: центрируем аватар + val avatarX = (screenWidthDp - avatarWidth) / 2 - // Позиция Y: от 0 (верх экрана) до обычной позиции, потом уходит вверх - val avatarStartY = 0.dp // Начинаем с самого верха (закрываем status bar) - val avatarMidY = statusBarHeight + 32.dp // Средняя позиция (круглый аватар) - val avatarEndY = statusBarHeight - 60.dp // Уходит вверх за экран - - val avatarY = if (collapseProgress < 0.5f) { - // Первая половина: от 0 до обычной позиции - androidx.compose.ui.unit.lerp(avatarStartY, avatarMidY, collapseProgress * 2f) + // Позиция Y: от 0 до центра header, потом уходит вверх + val avatarY = if (collapseProgress < 0.4f) { + // Фаза 1: остаётся наверху + 0.dp + } else if (collapseProgress < 0.7f) { + // Фаза 2: опускается в позицию круга + val phase2Progress = (collapseProgress - 0.4f) / 0.3f + androidx.compose.ui.unit.lerp(0.dp, statusBarHeight + 32.dp, phase2Progress) } else { - // Вторая половина: уходит вверх - androidx.compose.ui.unit.lerp(avatarMidY, avatarEndY, (collapseProgress - 0.5f) * 2f) + // Фаза 3: уходит вверх за экран + val phase3Progress = (collapseProgress - 0.7f) / 0.3f + androidx.compose.ui.unit.lerp(statusBarHeight + 32.dp, statusBarHeight - 60.dp, phase3Progress) } // Закругление: от 0 (квадрат) до половины размера (круг) - val cornerRadius = androidx.compose.ui.unit.lerp(0.dp, avatarSize / 2, collapseProgress.coerceIn(0f, 0.3f) / 0.3f) + val cornerRadius = if (collapseProgress < 0.3f) { + androidx.compose.ui.unit.lerp(0.dp, avatarSize / 2, collapseProgress / 0.3f) + } else { + avatarSize / 2 // Полный круг + } val avatarFontSize = androidx.compose.ui.unit.lerp(40.sp, 0.sp, collapseProgress) @@ -736,18 +777,19 @@ private fun CollapsingProfileHeader( ) { // Используем AvatarImage если репозиторий доступен if (avatarRepository != null) { - // При collapseProgress < 0.2 - fullscreen аватар - if (collapseProgress < 0.2f) { + // При collapseProgress < 0.35 - fullscreen аватар (ещё не полностью круглый) + if (collapseProgress < 0.35f) { FullSizeAvatar( publicKey = publicKey, - avatarRepository = avatarRepository + avatarRepository = avatarRepository, + isDarkTheme = isDarkTheme ) } else { AvatarImage( publicKey = publicKey, avatarRepository = avatarRepository, - size = avatarSize - 4.dp, - isDarkTheme = false, + size = avatarSize, + isDarkTheme = isDarkTheme, onClick = null, showOnlineIndicator = false ) @@ -827,7 +869,12 @@ private fun CollapsingProfileHeader( onSetPhotoClick = { onAvatarMenuChange(false) onSetPhotoClick() - } + }, + onDeletePhotoClick = { + onAvatarMenuChange(false) + onDeletePhotoClick() + }, + hasAvatar = hasAvatar ) } @@ -896,7 +943,8 @@ private fun CollapsingProfileHeader( @Composable private fun FullSizeAvatar( publicKey: String, - avatarRepository: AvatarRepository? + avatarRepository: AvatarRepository?, + isDarkTheme: Boolean = false ) { val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState() ?: remember { mutableStateOf(emptyList()) } @@ -921,7 +969,7 @@ private fun FullSizeAvatar( contentScale = ContentScale.Crop ) } else { - val avatarColors = getAvatarColor(publicKey, false) + val avatarColors = getAvatarColor(publicKey, isDarkTheme) Box( modifier = Modifier .fillMaxSize()