413 lines
17 KiB
Kotlin
413 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)
|
|
) {
|
|
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) { 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)
|
|
) {
|
|
items(displayGifs) { 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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|