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")
|
@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 последних)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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,
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user