Files
forkmessager/client-mobile/chats/presentation/components/MediaPicker.kt
2026-04-28 00:16:52 +03:00

417 lines
17 KiB
Kotlin

package chats.presentation.components
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import chats.data.remote.api.KlipyGifDto
import core.utils.CoilUtils
import kotlinx.coroutines.launch
enum class MediaTab { EMOJI, GIF }
@Composable
fun MediaPicker(
trendingGifs: List<KlipyGifDto>,
searchedGifs: List<KlipyGifDto>,
recentGifs: List<KlipyGifDto> = emptyList(),
gifCategories: List<chats.data.remote.api.GifCategoryDto> = emptyList(),
isGifsLoading: Boolean,
error: String?,
onEmojiSelected: (String) -> Unit,
onGifSelected: (String) -> Unit,
onGifSearch: (String) -> Unit,
modifier: Modifier = Modifier
) {
var activeTab by remember { mutableStateOf(MediaTab.EMOJI) }
// Вычисляем индексы для прыжков (учитываем заголовки)
val emojiCategories = remember {
listOf(
EmojiCategory("Недавние", Icons.Rounded.Schedule, listOf("👍", "❤️", "😂", "😍", "🙏", "🔥", "😭", "😊")),
EmojiCategory("Смайлы", Icons.Rounded.EmojiEmotions, listOf("😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃", "😉", "😊", "😇")),
EmojiCategory("Животные", Icons.Rounded.Pets, listOf("🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐨", "🐯", "🦁", "🐮", "🐷")),
EmojiCategory("Еда", Icons.Rounded.Fastfood, listOf("🍎", "🍐", "🍊", "🍋", "🍌", "🍉", "🍇", "🍓", "🫐", "🍈", "🍒", "🍑", "🥭")),
EmojiCategory("Спорт", Icons.Rounded.SportsFootball, listOf("", "🏀", "🏈", "", "🎾", "🏐", "🏉", "🎱", "🏓", "🏸", "🥅", "🏒")),
EmojiCategory("Транспорт", Icons.Rounded.DirectionsCar, listOf("🚗", "🚕", "🚙", "🚌", "🚎", "🏎️", "🚓", "🚑", "🚒", "🚐", "🚚", "🚛")),
EmojiCategory("Объекты", Icons.Rounded.Lightbulb, listOf("💡", "🔦", "🕯️", "🪔", "📔", "📕", "📖", "💚", "💙", "💜", "🤎", "🖤")),
EmojiCategory("Музыка", Icons.Rounded.MusicNote, listOf("🎵", "🎶", "🎼", "🎹", "🎸", "🎻", "🎷", "🎺", "🥁", "🎤", "🎧", "📻")),
EmojiCategory("Флаги", Icons.Rounded.Flag, listOf("🏁", "🚩", "🎌", "🏴", "🏳️", "🏳️‍🌈", "🏳️‍⚧️", "🏴‍☠️", "🇦🇱", "🇩🇿", "🇦🇸", "🇦🇩"))
)
}
val categoryIndices = remember(emojiCategories) {
var currentIdx = 0
emojiCategories.map { cat ->
val startIdx = currentIdx
currentIdx += 1 + cat.emojis.size
cat.icon to startIdx
}
}
Column(
modifier = modifier
.fillMaxWidth()
.height(450.dp)
.background(Color(0xFF1C1C1C), RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp))
) {
// Табы (Эмодзи / GIF)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.background(Color(0xFF2C2C2C), RoundedCornerShape(12.dp))
.padding(2.dp)
) {
MediaTabItem(
title = "ЭМОДЗИ",
isSelected = activeTab == MediaTab.EMOJI,
onClick = { activeTab = MediaTab.EMOJI },
modifier = Modifier.weight(1f)
)
MediaTabItem(
title = "GIF",
isSelected = activeTab == MediaTab.GIF,
onClick = { activeTab = MediaTab.GIF },
modifier = Modifier.weight(1f)
)
}
Crossfade(targetState = activeTab, label = "media_tab") { tab ->
when (tab) {
MediaTab.EMOJI -> EmojiContent(
emojiCategories = emojiCategories,
categoryIndices = categoryIndices,
onEmojiSelected = onEmojiSelected
)
MediaTab.GIF -> GifContent(
trendingGifs = trendingGifs,
searchedGifs = searchedGifs,
recentGifs = recentGifs,
gifCategories = gifCategories,
isGifsLoading = isGifsLoading,
error = error,
onGifSelected = onGifSelected,
onGifSearch = onGifSearch
)
}
}
}
}
@Composable
private fun MediaTabItem(
title: String,
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.clip(RoundedCornerShape(10.dp))
.background(if (isSelected) Color(0xFF3C3C3C) else Color.Transparent)
.clickable { onClick() }
.padding(vertical = 8.dp),
contentAlignment = Alignment.Center
) {
Text(
text = title,
color = if (isSelected) Color.White else Color.Gray,
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold
)
}
}
@Composable
private fun EmojiContent(
emojiCategories: List<EmojiCategory>,
categoryIndices: List<Pair<ImageVector, Int>>,
onEmojiSelected: (String) -> Unit
) {
val scrollState = androidx.compose.foundation.lazy.grid.rememberLazyGridState()
val scope = rememberCoroutineScope()
Column(modifier = Modifier.fillMaxSize()) {
// Категории
LazyRow(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
items(categoryIndices) { (icon, index) ->
Icon(
imageVector = icon,
contentDescription = null,
tint = Color.Gray,
modifier = Modifier
.size(24.dp)
.clickable {
scope.launch {
scrollState.animateScrollToItem(index)
}
}
)
}
}
// Поиск
TextField(
value = "",
onValueChange = {},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.clip(RoundedCornerShape(8.dp)),
placeholder = { Text("Поиск", color = Color.Gray) },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, tint = Color.Gray) },
colors = TextFieldDefaults.colors(
focusedContainerColor = Color(0xFF2C2C2C),
unfocusedContainerColor = Color(0xFF2C2C2C),
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
)
)
LazyVerticalGrid(
columns = GridCells.Fixed(8),
state = scrollState,
modifier = Modifier.weight(1f).padding(horizontal = 4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
emojiCategories.forEach { category ->
// Заголовок категории
item(span = { androidx.compose.foundation.lazy.grid.GridItemSpan(8) }) {
Text(
text = category.name,
color = Color.Gray,
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(8.dp)
)
}
items(category.emojis, key = { emoji -> emoji }) { emoji ->
Box(
modifier = Modifier
.aspectRatio(1f)
.clickable { onEmojiSelected(emoji) },
contentAlignment = Alignment.Center
) {
Text(text = emoji, fontSize = 28.sp)
}
}
}
}
}
}
data class EmojiCategory(val name: String, val icon: ImageVector, val emojis: List<String>)
@Composable
private fun GifContent(
trendingGifs: List<KlipyGifDto>,
searchedGifs: List<KlipyGifDto>,
recentGifs: List<KlipyGifDto>,
gifCategories: List<chats.data.remote.api.GifCategoryDto>,
isGifsLoading: Boolean,
error: String?,
onGifSelected: (String) -> Unit,
onGifSearch: (String) -> Unit
) {
var query by remember { mutableStateOf("") }
var subTab by remember { mutableStateOf("trending") }
val context = LocalContext.current
val imageLoader = CoilUtils.getGifImageLoader(context)
Column(modifier = Modifier.fillMaxSize()) {
TextField(
value = query,
onValueChange = {
query = it
if (it.isNotEmpty()) subTab = "search"
else if (subTab == "search") subTab = "trending"
onGifSearch(it)
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp)
.clip(RoundedCornerShape(8.dp)),
placeholder = { Text("Поиск GIF...", color = Color.Gray) },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, tint = Color.Gray) },
colors = TextFieldDefaults.colors(
focusedContainerColor = Color(0xFF2C2C2C),
unfocusedContainerColor = Color(0xFF2C2C2C),
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
)
)
// Под-вкладки GIF (Recent, Trending, Categories)
Row(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Rounded.Schedule,
contentDescription = null,
tint = if (subTab == "recent") Color(0xFF3B82F6) else Color.Gray,
modifier = Modifier.size(20.dp).clickable { subTab = "recent"; query = "" }
)
Icon(
Icons.Rounded.TrendingUp,
contentDescription = null,
tint = if (subTab == "trending") Color(0xFF3B82F6) else Color.Gray,
modifier = Modifier.size(20.dp).clickable { subTab = "trending"; query = "" }
)
Icon(
Icons.Rounded.GridView,
contentDescription = null,
tint = if (subTab == "categories") Color(0xFF3B82F6) else Color.Gray,
modifier = Modifier.size(20.dp).clickable { subTab = "categories"; query = "" }
)
}
if (subTab == "categories") {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.weight(1f).padding(4.dp),
contentPadding = PaddingValues(4.dp)
) {
items(gifCategories) { category ->
Box(
modifier = Modifier
.padding(4.dp)
.aspectRatio(1.77f)
.clip(RoundedCornerShape(12.dp))
.background(Color(0xFF2C2C2C))
.clickable {
query = category.query
subTab = "search"
onGifSearch(category.query)
}
) {
AsyncImage(
model = category.preview_url,
contentDescription = category.category,
modifier = Modifier.fillMaxSize().alpha(0.6f),
contentScale = ContentScale.Crop
)
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = category.category.uppercase(),
color = Color.White,
fontSize = 10.sp,
fontWeight = FontWeight.Black,
letterSpacing = 2.sp
)
}
}
}
}
} else {
val displayGifs = when (subTab) {
"recent" -> recentGifs
"search" -> searchedGifs
else -> trendingGifs
}
if (isGifsLoading && displayGifs.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(color = Color(0xFF3B82F6))
}
} else if (!isGifsLoading && displayGifs.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(error ?: "Ничего не найдено", color = Color.Gray)
if (error != null) {
Button(onClick = { onGifSearch(query) }) {
Text("Повторить")
}
}
}
}
} else {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier.weight(1f).padding(4.dp),
contentPadding = PaddingValues(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
items(displayGifs, key = { gif -> gif.id }, contentType = { "gif" }) { gif ->
val url = gif.files?.get("hd")?.get("gif")?.url
?: gif.files?.get("sd")?.get("gif")?.url
?: gif.file?.get("hd")?.get("gif")?.url
?: gif.file?.get("sd")?.get("gif")?.url
?: gif.media_formats?.get("gif")?.url
?: gif.images?.original?.url
val preview = gif.files?.get("sd")?.get("webp")?.url
?: gif.files?.get("sd")?.get("gif")?.url
?: gif.file?.get("sd")?.get("webp")?.url
?: gif.media_formats?.get("tinygif")?.url
?: gif.images?.fixed_height_small?.url
?: url
if (url != null) {
AsyncImage(
model = preview ?: url,
imageLoader = imageLoader,
contentDescription = gif.title,
modifier = Modifier
.padding(2.dp)
.aspectRatio(1f)
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFF2C2C2C))
.clickable { onGifSelected(url) },
contentScale = ContentScale.Crop
)
} else {
Box(
modifier = Modifier
.padding(2.dp)
.aspectRatio(1f)
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFF3C3C3C)),
contentAlignment = Alignment.Center
) {
Text(gif.title ?: "GIF", color = Color.Gray, fontSize = 10.sp)
}
}
}
}
}
}
}
}