feat: Add avatar deletion functionality and update ProfileScreen to handle avatar presence
This commit is contained in:
@@ -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 последних)
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузить и расшифровать аватар из файла
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user