feat: Add avatar deletion functionality and update ProfileScreen to handle avatar presence

This commit is contained in:
k1ngsterr1
2026-01-30 03:36:01 +05:00
parent 7691926ef6
commit 5091eb557a
4 changed files with 149 additions and 29 deletions

View File

@@ -67,6 +67,18 @@ interface AvatarDao {
@Query("DELETE FROM avatar_cache WHERE public_key = :publicKey") @Query("DELETE FROM avatar_cache WHERE public_key = :publicKey")
suspend fun deleteAvatars(publicKey: String) 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<AvatarCacheEntity>
/** /**
* Удалить старые аватары (оставить только N последних) * Удалить старые аватары (оставить только N последних)
*/ */

View File

@@ -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
}
}
}
/** /**
* Загрузить и расшифровать аватар из файла * Загрузить и расшифровать аватар из файла
*/ */

View File

@@ -1137,8 +1137,12 @@ fun ProfilePhotoMenu(
expanded: Boolean, expanded: Boolean,
onDismiss: () -> Unit, onDismiss: () -> Unit,
isDarkTheme: Boolean, 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( DropdownMenu(
expanded = expanded, expanded = expanded,
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
@@ -1156,6 +1160,25 @@ fun ProfilePhotoMenu(
tintColor = if (isDarkTheme) Color.White else Color.Black, tintColor = if (isDarkTheme) Color.White else Color.Black,
textColor = 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)
)
}
} }
} }

View File

@@ -268,6 +268,11 @@ fun ProfileScreen(
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val avatarColors = getAvatarColor(accountPublicKey, isDarkTheme) 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 // State for editing - Update when account data changes
var editedName by remember(accountName) { mutableStateOf(accountName) } var editedName by remember(accountName) { mutableStateOf(accountName) }
var editedUsername by remember(accountUsername) { mutableStateOf(accountUsername) } var editedUsername by remember(accountUsername) { mutableStateOf(accountUsername) }
@@ -520,6 +525,18 @@ fun ProfileScreen(
onSetPhotoClick = { onSetPhotoClick = {
imagePickerLauncher.launch("image/*") 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 avatarRepository = avatarRepository
) )
} }
@@ -636,6 +653,8 @@ private fun CollapsingProfileHeader(
showAvatarMenu: Boolean, showAvatarMenu: Boolean,
onAvatarMenuChange: (Boolean) -> Unit, onAvatarMenuChange: (Boolean) -> Unit,
onSetPhotoClick: () -> Unit, onSetPhotoClick: () -> Unit,
onDeletePhotoClick: () -> Unit,
hasAvatar: Boolean,
avatarRepository: AvatarRepository? avatarRepository: AvatarRepository?
) { ) {
val density = LocalDensity.current val density = LocalDensity.current
@@ -654,35 +673,57 @@ private fun CollapsingProfileHeader(
val headerHeight = androidx.compose.ui.unit.lerp(expandedHeight, collapsedHeight, collapseProgress) val headerHeight = androidx.compose.ui.unit.lerp(expandedHeight, collapsedHeight, collapseProgress)
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// 👤 AVATAR - По умолчанию квадратный во весь экран, при скролле становится круглым // 👤 AVATAR - По умолчанию прямоугольный во весь header, при скролле становится круглым
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// Размер: ширина = screenWidthDp (полная ширина), высота = expandedHeight // Размеры аватара - всегда помещается в header
// При скролле уменьшается до 0 // При collapseProgress=0: ширина=screenWidth, высота=expandedHeight
val avatarWidth = androidx.compose.ui.unit.lerp(screenWidthDp, 0.dp, collapseProgress) // При collapseProgress=1: размер=0
val avatarHeight = androidx.compose.ui.unit.lerp(expandedHeight, 0.dp, collapseProgress)
// Для 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) val avatarSize = minOf(avatarWidth, avatarHeight)
// Позиция X: от 0 (весь экран) до центра // Позиция X: центрируем аватар
val avatarCenterX = (screenWidthDp - AVATAR_SIZE_EXPANDED) / 2 val avatarX = (screenWidthDp - avatarWidth) / 2
val avatarX = androidx.compose.ui.unit.lerp(0.dp, avatarCenterX + (AVATAR_SIZE_EXPANDED - avatarSize) / 2, collapseProgress.coerceIn(0f, 0.5f) * 2f)
// Позиция Y: от 0 (верх экрана) до обычной позиции, потом уходит вверх // Позиция Y: от 0 до центра header, потом уходит вверх
val avatarStartY = 0.dp // Начинаем с самого верха (закрываем status bar) val avatarY = if (collapseProgress < 0.4f) {
val avatarMidY = statusBarHeight + 32.dp // Средняя позиция (круглый аватар) // Фаза 1: остаётся наверху
val avatarEndY = statusBarHeight - 60.dp // Уходит вверх за экран 0.dp
} else if (collapseProgress < 0.7f) {
val avatarY = if (collapseProgress < 0.5f) { // Фаза 2: опускается в позицию круга
// Первая половина: от 0 до обычной позиции val phase2Progress = (collapseProgress - 0.4f) / 0.3f
androidx.compose.ui.unit.lerp(avatarStartY, avatarMidY, collapseProgress * 2f) androidx.compose.ui.unit.lerp(0.dp, statusBarHeight + 32.dp, phase2Progress)
} else { } else {
// Вторая половина: уходит вверх // Фаза 3: уходит вверх за экран
androidx.compose.ui.unit.lerp(avatarMidY, avatarEndY, (collapseProgress - 0.5f) * 2f) val phase3Progress = (collapseProgress - 0.7f) / 0.3f
androidx.compose.ui.unit.lerp(statusBarHeight + 32.dp, statusBarHeight - 60.dp, phase3Progress)
} }
// Закругление: от 0 (квадрат) до половины размера (круг) // Закругление: от 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) val avatarFontSize = androidx.compose.ui.unit.lerp(40.sp, 0.sp, collapseProgress)
@@ -736,18 +777,19 @@ private fun CollapsingProfileHeader(
) { ) {
// Используем AvatarImage если репозиторий доступен // Используем AvatarImage если репозиторий доступен
if (avatarRepository != null) { if (avatarRepository != null) {
// При collapseProgress < 0.2 - fullscreen аватар // При collapseProgress < 0.35 - fullscreen аватар (ещё не полностью круглый)
if (collapseProgress < 0.2f) { if (collapseProgress < 0.35f) {
FullSizeAvatar( FullSizeAvatar(
publicKey = publicKey, publicKey = publicKey,
avatarRepository = avatarRepository avatarRepository = avatarRepository,
isDarkTheme = isDarkTheme
) )
} else { } else {
AvatarImage( AvatarImage(
publicKey = publicKey, publicKey = publicKey,
avatarRepository = avatarRepository, avatarRepository = avatarRepository,
size = avatarSize - 4.dp, size = avatarSize,
isDarkTheme = false, isDarkTheme = isDarkTheme,
onClick = null, onClick = null,
showOnlineIndicator = false showOnlineIndicator = false
) )
@@ -827,7 +869,12 @@ private fun CollapsingProfileHeader(
onSetPhotoClick = { onSetPhotoClick = {
onAvatarMenuChange(false) onAvatarMenuChange(false)
onSetPhotoClick() onSetPhotoClick()
} },
onDeletePhotoClick = {
onAvatarMenuChange(false)
onDeletePhotoClick()
},
hasAvatar = hasAvatar
) )
} }
@@ -896,7 +943,8 @@ private fun CollapsingProfileHeader(
@Composable @Composable
private fun FullSizeAvatar( private fun FullSizeAvatar(
publicKey: String, publicKey: String,
avatarRepository: AvatarRepository? avatarRepository: AvatarRepository?,
isDarkTheme: Boolean = false
) { ) {
val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState() val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
?: remember { mutableStateOf(emptyList()) } ?: remember { mutableStateOf(emptyList()) }
@@ -921,7 +969,7 @@ private fun FullSizeAvatar(
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
} else { } else {
val avatarColors = getAvatarColor(publicKey, false) val avatarColors = getAvatarColor(publicKey, isDarkTheme)
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()