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")
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 последних)
*/

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,
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)
)
}
}
}

View File

@@ -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()