Гиф
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -39,13 +39,13 @@ interface ChatApi {
|
|||||||
|
|
||||||
// Klipy GIF API
|
// Klipy GIF API
|
||||||
@GET("klipy/trending")
|
@GET("klipy/trending")
|
||||||
suspend fun getTrendingGifs(@Query("page") page: Int): List<KlipyGifDto>
|
suspend fun getTrendingGifs(@Query("page") page: Int): KlipyResponse
|
||||||
|
|
||||||
@GET("klipy/search")
|
@GET("klipy/search")
|
||||||
suspend fun searchGifs(@Query("q") query: String, @Query("page") page: Int): List<KlipyGifDto>
|
suspend fun searchGifs(@Query("q") query: String, @Query("page") page: Int): KlipyResponse
|
||||||
|
|
||||||
@GET("klipy/categories")
|
@GET("klipy/categories")
|
||||||
suspend fun getGifCategories(): List<GifCategoryDto>
|
suspend fun getGifCategories(): GifCategoriesResponse
|
||||||
|
|
||||||
@POST("klipy/shared/{id}")
|
@POST("klipy/shared/{id}")
|
||||||
suspend fun markGifShared(@Path("id") id: String, @Body query: String)
|
suspend fun markGifShared(@Path("id") id: String, @Body query: String)
|
||||||
@@ -60,15 +60,27 @@ interface ChatApi {
|
|||||||
suspend fun markMessagesAsRead(@Path("chatId") chatId: String, @Body lastMessageId: String)
|
suspend fun markMessagesAsRead(@Path("chatId") chatId: String, @Body lastMessageId: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class KlipyResponse(
|
||||||
|
val data: KlipyDataWrapper
|
||||||
|
)
|
||||||
|
|
||||||
|
data class KlipyDataWrapper(
|
||||||
|
val data: List<KlipyGifDto>
|
||||||
|
)
|
||||||
|
|
||||||
data class KlipyGifDto(
|
data class KlipyGifDto(
|
||||||
val id: String,
|
val id: String,
|
||||||
val images: GifImagesDto?,
|
val images: GifImagesDto? = null,
|
||||||
val title: String?
|
val files: Map<String, Map<String, GifImageSourceDto>>? = null,
|
||||||
|
val file: Map<String, Map<String, GifImageSourceDto>>? = null,
|
||||||
|
val media_formats: Map<String, GifImageSourceDto>? = null,
|
||||||
|
val title: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GifImagesDto(
|
data class GifImagesDto(
|
||||||
val fixed_height: GifImageSourceDto?,
|
val fixed_height: GifImageSourceDto? = null,
|
||||||
val original: GifImageSourceDto?
|
val original: GifImageSourceDto? = null,
|
||||||
|
val fixed_height_small: GifImageSourceDto? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GifImageSourceDto(
|
data class GifImageSourceDto(
|
||||||
@@ -86,3 +98,11 @@ data class FileUploadResponse(
|
|||||||
val filename: String,
|
val filename: String,
|
||||||
val size: Long
|
val size: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class GifCategoriesResponse(
|
||||||
|
val data: GifCategoriesData
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GifCategoriesData(
|
||||||
|
val categories: List<GifCategoryDto>
|
||||||
|
)
|
||||||
|
|||||||
@@ -55,6 +55,18 @@ class ChatRepositoryImpl @Inject constructor(
|
|||||||
val body = MultipartBody.Part.createFormData("file", file.name, requestFile)
|
val body = MultipartBody.Part.createFormData("file", file.name, requestFile)
|
||||||
return api.uploadFile(body).url
|
return api.uploadFile(body).url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getTrendingGifs(page: Int): List<chats.data.remote.api.KlipyGifDto> {
|
||||||
|
return api.getTrendingGifs(page).data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun searchGifs(query: String, page: Int): List<chats.data.remote.api.KlipyGifDto> {
|
||||||
|
return api.searchGifs(query, page).data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getGifCategories(): List<chats.data.remote.api.GifCategoryDto> {
|
||||||
|
return api.getGifCategories().data.categories
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mappers
|
// Mappers
|
||||||
|
|||||||
@@ -11,5 +11,8 @@ interface ChatRepository {
|
|||||||
suspend fun sendTypingStatus(chatId: String)
|
suspend fun sendTypingStatus(chatId: String)
|
||||||
suspend fun markMessagesAsRead(chatId: String, lastMessageId: String)
|
suspend fun markMessagesAsRead(chatId: String, lastMessageId: String)
|
||||||
suspend fun uploadMedia(file: java.io.File): String
|
suspend fun uploadMedia(file: java.io.File): String
|
||||||
|
suspend fun getTrendingGifs(page: Int = 0): List<chats.data.remote.api.KlipyGifDto>
|
||||||
|
suspend fun searchGifs(query: String, page: Int = 0): List<chats.data.remote.api.KlipyGifDto>
|
||||||
|
suspend fun getGifCategories(): List<chats.data.remote.api.GifCategoryDto>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chats.presentation.components.EmojiPicker
|
import chats.presentation.components.MediaPicker
|
||||||
import chats.presentation.components.MessageBubble
|
import chats.presentation.components.MessageBubble
|
||||||
import core.presentation.components.AppAvatar
|
import core.presentation.components.AppAvatar
|
||||||
import core.presentation.components.AppMediaLightbox
|
import core.presentation.components.AppMediaLightbox
|
||||||
@@ -246,7 +246,30 @@ fun ChatDetailScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isEmojiPickerVisible) {
|
if (isEmojiPickerVisible) {
|
||||||
EmojiPicker(onEmojiSelected = { textInput += it })
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.loadTrendingGifs()
|
||||||
|
}
|
||||||
|
MediaPicker(
|
||||||
|
trendingGifs = state.trendingGifs,
|
||||||
|
searchedGifs = state.searchedGifs,
|
||||||
|
recentGifs = state.recentGifs,
|
||||||
|
gifCategories = state.gifCategories,
|
||||||
|
isGifsLoading = state.isGifsLoading,
|
||||||
|
error = state.error,
|
||||||
|
onEmojiSelected = { textInput += it },
|
||||||
|
onGifSelected = { url ->
|
||||||
|
viewModel.sendGif(url)
|
||||||
|
isEmojiPickerVisible = false
|
||||||
|
},
|
||||||
|
onGifSearch = { query ->
|
||||||
|
viewModel.searchGifs(query)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.loadTrendingGifs()
|
||||||
|
viewModel.loadGifCategories()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import kotlinx.coroutines.launch
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
import chats.data.remote.api.KlipyGifDto
|
||||||
|
|
||||||
data class ChatDetailState(
|
data class ChatDetailState(
|
||||||
val messages: List<Message> = emptyList(),
|
val messages: List<Message> = emptyList(),
|
||||||
val chatName: String? = null,
|
val chatName: String? = null,
|
||||||
@@ -26,7 +28,12 @@ data class ChatDetailState(
|
|||||||
val typingUser: String? = null,
|
val typingUser: String? = null,
|
||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
val canCall: Boolean = true,
|
val canCall: Boolean = true,
|
||||||
val maxFileSize: Long = 100 * 1024 * 1024
|
val maxFileSize: Long = 100 * 1024 * 1024,
|
||||||
|
val trendingGifs: List<KlipyGifDto> = emptyList(),
|
||||||
|
val searchedGifs: List<KlipyGifDto> = emptyList(),
|
||||||
|
val recentGifs: List<KlipyGifDto> = emptyList(),
|
||||||
|
val gifCategories: List<chats.data.remote.api.GifCategoryDto> = emptyList(),
|
||||||
|
val isGifsLoading: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -182,5 +189,78 @@ class ChatDetailViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadTrendingGifs() {
|
||||||
|
if (_state.value.trendingGifs.isNotEmpty()) return
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isGifsLoading = true) }
|
||||||
|
try {
|
||||||
|
val gifs = repository.getTrendingGifs(0)
|
||||||
|
_state.update { it.copy(trendingGifs = gifs, isGifsLoading = false) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update { it.copy(isGifsLoading = false, error = "GIF load error: ${e.message}") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadGifCategories() {
|
||||||
|
if (_state.value.gifCategories.isNotEmpty()) return
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val categories = repository.getGifCategories()
|
||||||
|
_state.update { it.copy(gifCategories = categories) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Ignore failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var searchJob: Job? = null
|
||||||
|
fun searchGifs(query: String) {
|
||||||
|
searchJob?.cancel()
|
||||||
|
if (query.isBlank()) {
|
||||||
|
_state.update { it.copy(searchedGifs = emptyList()) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
searchJob = viewModelScope.launch {
|
||||||
|
delay(500)
|
||||||
|
_state.update { it.copy(isGifsLoading = true) }
|
||||||
|
try {
|
||||||
|
val gifs = repository.searchGifs(query, 0)
|
||||||
|
_state.update { it.copy(searchedGifs = gifs, isGifsLoading = false) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update { it.copy(isGifsLoading = false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendGif(url: String) {
|
||||||
|
val chatId = currentChatId ?: return
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
repository.sendMessage(chatId, url)
|
||||||
|
|
||||||
|
// Add to recent
|
||||||
|
val allGifs = _state.value.trendingGifs + _state.value.searchedGifs + _state.value.recentGifs
|
||||||
|
val selectedGif = allGifs.find { gif ->
|
||||||
|
val gifUrl = 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
|
||||||
|
gifUrl == url
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedGif?.let { gif ->
|
||||||
|
val newList = (listOf(gif) + _state.value.recentGifs).distinctBy { it.id }.take(20)
|
||||||
|
_state.update { it.copy(recentGifs = newList) }
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update { it.copy(error = e.localizedMessage) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,412 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user