feat: Enhance AppleEmojiPicker to load all emojis from assets and group them by category
This commit is contained in:
@@ -9,7 +9,6 @@ import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -56,11 +55,8 @@ import com.rosetta.messenger.ui.components.VerifiedBadge
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.view.ViewTreeObserver
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -176,10 +172,11 @@ fun ChatDetailScreen(
|
||||
) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF0E0E0E) else Color(0xFFEFEFF3)
|
||||
// Цвета как в React Native themes.ts
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF1E1E1E) else Color(0xFFFFFFFF)
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
val inputBackgroundColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF0F0F0)
|
||||
val inputBackgroundColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF2F3F5)
|
||||
|
||||
// <20> Fade-in анимация для всего экрана
|
||||
var isVisible by remember { mutableStateOf(false) }
|
||||
@@ -288,40 +285,13 @@ fun ChatDetailScreen(
|
||||
// 🔥 Обработка системной кнопки назад
|
||||
BackHandler { hideKeyboardAndBack() }
|
||||
|
||||
// 🔥 Ручное отслеживание высоты клавиатуры
|
||||
val view = LocalView.current
|
||||
val density = LocalDensity.current
|
||||
var keyboardHeight by remember { mutableStateOf(0.dp) }
|
||||
|
||||
DisposableEffect(view) {
|
||||
val listener = ViewTreeObserver.OnGlobalLayoutListener {
|
||||
val rect = Rect()
|
||||
view.getWindowVisibleDisplayFrame(rect)
|
||||
val screenHeight = view.rootView.height
|
||||
val keyboardHeightPx = screenHeight - rect.bottom
|
||||
|
||||
// Учитываем navigation bar (если клавиатура меньше 150px - это не клавиатура)
|
||||
val actualKeyboardHeight = if (keyboardHeightPx > 150) keyboardHeightPx else 0
|
||||
|
||||
keyboardHeight = with(density) { actualKeyboardHeight.toDp() }
|
||||
}
|
||||
|
||||
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
|
||||
|
||||
// 🔥 Cleanup при выходе из экрана
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
view.viewTreeObserver.removeOnGlobalLayoutListener(listener)
|
||||
focusManager.clearFocus()
|
||||
keyboardController?.hide()
|
||||
}
|
||||
}
|
||||
|
||||
// Автоматический скролл вниз при появлении клавиатуры
|
||||
LaunchedEffect(keyboardHeight) {
|
||||
if (keyboardHeight > 0.dp && messages.isNotEmpty()) {
|
||||
delay(50)
|
||||
listState.animateScrollToItem(0)
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализируем ViewModel с ключами и открываем диалог
|
||||
LaunchedEffect(user.publicKey) {
|
||||
@@ -354,7 +324,10 @@ fun ChatDetailScreen(
|
||||
)
|
||||
|
||||
// 🚀 Весь контент с fade-in анимацией
|
||||
Box(modifier = Modifier.fillMaxSize().graphicsLayer { alpha = screenAlpha }) {
|
||||
Box(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer { alpha = screenAlpha }
|
||||
) {
|
||||
// Telegram-style solid header background (без blur)
|
||||
val headerBackground = if (isDarkTheme) Color(0xFF212121) else Color(0xFFFFFFFF)
|
||||
|
||||
@@ -577,23 +550,17 @@ fun ChatDetailScreen(
|
||||
)
|
||||
}
|
||||
},
|
||||
containerColor = Color.Transparent
|
||||
containerColor = backgroundColor // Фон всего чата
|
||||
) { paddingValues ->
|
||||
// 🔥 Box с фиксированным Input внизу и сообщениями выше
|
||||
Box(
|
||||
// 🔥 Простой Column - сообщения сверху, инпут снизу
|
||||
// Клавиатура сама поднимает контент через adjustResize
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
// Список сообщений - занимает всё пространство, с padding снизу для input и клавиатуры
|
||||
val inputBarHeight = 60.dp
|
||||
val bottomPadding = inputBarHeight + keyboardHeight
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(bottom = bottomPadding)
|
||||
) {
|
||||
// Список сообщений - занимает всё доступное пространство
|
||||
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
|
||||
if (messages.isEmpty()) {
|
||||
// Пустое состояние
|
||||
Column(
|
||||
@@ -759,42 +726,35 @@ fun ChatDetailScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
} // Закрытие внутреннего Box для сообщений
|
||||
|
||||
// 🔥 INPUT BAR - фиксированный элемент внизу экрана
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = keyboardHeight) // Ручной padding над клавиатурой
|
||||
) {
|
||||
MessageInputBar(
|
||||
value = inputText,
|
||||
onValueChange = {
|
||||
viewModel.updateInputText(it)
|
||||
// Отправляем индикатор печатания
|
||||
if (it.isNotEmpty() && !isSavedMessages) {
|
||||
viewModel.sendTypingIndicator()
|
||||
}
|
||||
},
|
||||
onSend = {
|
||||
// Скрываем кнопку scroll на время отправки
|
||||
isSendingMessage = true
|
||||
viewModel.sendMessage()
|
||||
// Скроллим к новому сообщению
|
||||
scope.launch {
|
||||
delay(100)
|
||||
listState.animateScrollToItem(0)
|
||||
delay(300) // Ждём завершения анимации
|
||||
isSendingMessage = false
|
||||
}
|
||||
},
|
||||
isDarkTheme = isDarkTheme,
|
||||
backgroundColor = inputBackgroundColor,
|
||||
textColor = textColor,
|
||||
placeholderColor = secondaryTextColor
|
||||
)
|
||||
}
|
||||
|
||||
// 🔥 INPUT BAR - обычный элемент внизу Column
|
||||
MessageInputBar(
|
||||
value = inputText,
|
||||
onValueChange = {
|
||||
viewModel.updateInputText(it)
|
||||
// Отправляем индикатор печатания
|
||||
if (it.isNotEmpty() && !isSavedMessages) {
|
||||
viewModel.sendTypingIndicator()
|
||||
}
|
||||
},
|
||||
onSend = {
|
||||
// Скрываем кнопку scroll на время отправки
|
||||
isSendingMessage = true
|
||||
viewModel.sendMessage()
|
||||
// Скроллим к новому сообщению
|
||||
scope.launch {
|
||||
delay(100)
|
||||
listState.animateScrollToItem(0)
|
||||
delay(300) // Ждём завершения анимации
|
||||
isSendingMessage = false
|
||||
}
|
||||
},
|
||||
isDarkTheme = isDarkTheme,
|
||||
backgroundColor = inputBackgroundColor,
|
||||
textColor = textColor,
|
||||
placeholderColor = secondaryTextColor
|
||||
)
|
||||
}
|
||||
}
|
||||
} // Закрытие Box с fade-in
|
||||
@@ -1148,10 +1108,7 @@ private fun MessageInputBar(
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.background(Color.Transparent)
|
||||
// imePadding уже на родительском Column
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
// 🔥 TELEGRAM-STYLE FLOATING LIQUID GLASS INPUT
|
||||
Row(
|
||||
@@ -1344,9 +1301,5 @@ private fun MessageInputBar(
|
||||
onClose = { showEmojiPicker = false }
|
||||
)
|
||||
}
|
||||
|
||||
if (!showEmojiPicker) {
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ class AppleEmojiEditTextView @JvmOverloads constructor(
|
||||
init {
|
||||
// Настраиваем EditText
|
||||
background = null
|
||||
setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
||||
setPadding(0, 0, 0, 0)
|
||||
gravity = Gravity.CENTER_VERTICAL or Gravity.START
|
||||
isSingleLine = false
|
||||
|
||||
@@ -34,105 +34,150 @@ import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
|
||||
/**
|
||||
* Apple Emoji Picker с PNG изображениями
|
||||
* Категории и эмодзи как в React Native версии
|
||||
* Загружает ВСЕ эмодзи из assets/emoji директории
|
||||
*/
|
||||
|
||||
// Категории эмодзи
|
||||
data class EmojiCategory(
|
||||
val key: String,
|
||||
val label: String // unified код для иконки категории
|
||||
val label: String, // unified код для иконки категории
|
||||
val ranges: List<Pair<Int, Int>> // Unicode диапазоны для этой категории
|
||||
)
|
||||
|
||||
// Данные одного эмодзи
|
||||
data class EmojiItem(
|
||||
val unified: String,
|
||||
val shortName: String,
|
||||
val category: String
|
||||
)
|
||||
|
||||
// Категории как в RN
|
||||
// Стандартный порядок категорий как в iOS/Android/WhatsApp
|
||||
val EMOJI_CATEGORIES = listOf(
|
||||
EmojiCategory("Smileys & Emotion", "1f600"), // 😀
|
||||
EmojiCategory("People & Body", "1f44b"), // 👋
|
||||
EmojiCategory("Animals & Nature", "1f431"), // 🐱
|
||||
EmojiCategory("Food & Drink", "1f34e"), // 🍎
|
||||
EmojiCategory("Travel & Places", "2708-fe0f"), // ✈️
|
||||
EmojiCategory("Activities", "26bd"), // ⚽
|
||||
EmojiCategory("Objects", "1f4a1"), // 💡
|
||||
EmojiCategory("Symbols", "2764-fe0f"), // ❤️
|
||||
EmojiCategory("Flags", "1f3f3-fe0f") // 🏳️
|
||||
// 😀 Smileys & Emotion: лица, эмоции, руки
|
||||
EmojiCategory("Smileys", "1f600", listOf(
|
||||
0x1F600 to 0x1F64F, // Emoticons
|
||||
0x1F910 to 0x1F92F, // Face with...
|
||||
0x1F970 to 0x1F9FF, // More faces
|
||||
0x263A to 0x263A, // ☺
|
||||
0x2639 to 0x2639 // ☹
|
||||
)),
|
||||
// 👋 People & Body: люди, жесты, части тела
|
||||
EmojiCategory("People", "1f44b", listOf(
|
||||
0x1F466 to 0x1F4FF, // People
|
||||
0x1F9D0 to 0x1F9DF, // More people
|
||||
0x270A to 0x270D, // Hands
|
||||
0x261D to 0x261D, // ☝
|
||||
0x1F440 to 0x1F465 // Body parts
|
||||
)),
|
||||
// 🐱 Animals & Nature: животные, растения, природа
|
||||
EmojiCategory("Animals", "1f431", listOf(
|
||||
0x1F400 to 0x1F43F, // Animals
|
||||
0x1F980 to 0x1F9AE, // More animals
|
||||
0x1F330 to 0x1F335, // Plants
|
||||
0x1F337 to 0x1F34F, // Flowers, fruits
|
||||
0x2618 to 0x2618 // ☘
|
||||
)),
|
||||
// 🍎 Food & Drink: еда, напитки
|
||||
EmojiCategory("Food", "1f34e", listOf(
|
||||
0x1F345 to 0x1F37F, // Food
|
||||
0x1F950 to 0x1F96F, // More food
|
||||
0x1F9C0 to 0x1F9CB, // Cheese, drinks
|
||||
0x1FAD0 to 0x1FAD9, // New food
|
||||
0x2615 to 0x2615 // ☕
|
||||
)),
|
||||
// ✈️ Travel & Places: транспорт, места, здания
|
||||
EmojiCategory("Travel", "2708-fe0f", listOf(
|
||||
0x1F680 to 0x1F6FF, // Transport
|
||||
0x1F3D4 to 0x1F3DF, // Places
|
||||
0x1F3E0 to 0x1F3F0, // Buildings
|
||||
0x2708 to 0x2708, // ✈
|
||||
0x26F0 to 0x26FF // Mountains, etc
|
||||
)),
|
||||
// ⚽ Activities: спорт, игры, хобби
|
||||
EmojiCategory("Activities", "26bd", listOf(
|
||||
0x1F3A0 to 0x1F3CA, // Activities
|
||||
0x1F3CB to 0x1F3D3, // Sports
|
||||
0x1F93C to 0x1F94F, // More sports
|
||||
0x26BD to 0x26BE, // ⚽⚾
|
||||
0x265F to 0x2660, // Chess
|
||||
0x1F9E0 to 0x1F9FF // Games
|
||||
)),
|
||||
// 💡 Objects: предметы, инструменты
|
||||
EmojiCategory("Objects", "1f4a1", listOf(
|
||||
0x1F4A1 to 0x1F4FF, // Objects (lightbulb to...)
|
||||
0x1F500 to 0x1F5FF, // More objects
|
||||
0x1F6E0 to 0x1F6EF, // Tools
|
||||
0x1FA70 to 0x1FAFF, // New objects
|
||||
0x2328 to 0x2328 // ⌨
|
||||
)),
|
||||
// ❤️ Symbols: сердца, знаки, символы
|
||||
EmojiCategory("Symbols", "2764-fe0f", listOf(
|
||||
0x2764 to 0x2764, // ❤
|
||||
0x1F490 to 0x1F49F, // Hearts
|
||||
0x2600 to 0x26FF, // Misc symbols
|
||||
0x2700 to 0x27BF, // Dingbats
|
||||
0x1F170 to 0x1F1FF, // Squared letters (before flags)
|
||||
0x00A9 to 0x00AE, // ©®
|
||||
0x203C to 0x3299 // Misc
|
||||
)),
|
||||
// 🏳️ Flags: флаги стран
|
||||
EmojiCategory("Flags", "1f3f3-fe0f", listOf(
|
||||
0x1F1E0 to 0x1F1FF, // Regional indicators (flags)
|
||||
0x1F3F3 to 0x1F3F4, // 🏳🏴
|
||||
0x1F3C1 to 0x1F3C1, // 🏁
|
||||
0x1F6A9 to 0x1F6A9 // 🚩
|
||||
))
|
||||
)
|
||||
|
||||
// Эмодзи по категориям (основные)
|
||||
val EMOJIS_BY_CATEGORY = mapOf(
|
||||
"Smileys & Emotion" to listOf(
|
||||
"1f600", "1f603", "1f604", "1f601", "1f606", "1f605", "1f923", "1f602",
|
||||
"1f642", "1f643", "1f609", "1f60a", "1f607", "1f970", "1f60d", "1f929",
|
||||
"1f618", "1f617", "263a-fe0f", "1f61a", "1f619", "1f972", "1f60b", "1f61b",
|
||||
"1f61c", "1f92a", "1f61d", "1f911", "1f917", "1f92d", "1f92b", "1f914",
|
||||
"1f910", "1f928", "1f610", "1f611", "1f636", "1f60f", "1f612", "1f644",
|
||||
"1f62c", "1f925", "1f60c", "1f614", "1f62a", "1f924", "1f634", "1f637",
|
||||
"1f912", "1f915", "1f922", "1f92e", "1f927", "1f975", "1f976", "1f974",
|
||||
"1f635", "1f92f", "1f920", "1f973", "1f978", "1f60e", "1f913", "1f9d0"
|
||||
),
|
||||
"People & Body" to listOf(
|
||||
"1f44b", "1f91a", "1f590-fe0f", "270b", "1f596", "1f44c", "1f90c", "1f90f",
|
||||
"270c-fe0f", "1f91e", "1f91f", "1f918", "1f919", "1f448", "1f449", "1f446",
|
||||
"1f595", "1f447", "261d-fe0f", "1f44d", "1f44e", "270a", "1f44a", "1f91b",
|
||||
"1f91c", "1f44f", "1f64c", "1f450", "1f932", "1f91d", "1f64f", "270d-fe0f",
|
||||
"1f485", "1f933", "1f4aa", "1f9be", "1f9bf", "1f9b5", "1f9b6", "1f442",
|
||||
"1f9bb", "1f443", "1f9e0", "1fac0", "1fac1", "1f9b7", "1f9b4", "1f440"
|
||||
),
|
||||
"Animals & Nature" to listOf(
|
||||
"1f435", "1f412", "1f98d", "1f9a7", "1f436", "1f415", "1f9ae", "1f415-200d-1f9ba",
|
||||
"1f429", "1f43a", "1f98a", "1f99d", "1f431", "1f408", "1f408-200d-2b1b", "1f981",
|
||||
"1f42f", "1f405", "1f406", "1f434", "1f40e", "1f984", "1f993", "1f98c",
|
||||
"1f9ac", "1f42e", "1f402", "1f403", "1f404", "1f437", "1f416", "1f417",
|
||||
"1f43d", "1f40f", "1f411", "1f410", "1f42a", "1f42b", "1f999", "1f992"
|
||||
),
|
||||
"Food & Drink" to listOf(
|
||||
"1f347", "1f348", "1f349", "1f34a", "1f34b", "1f34c", "1f34d", "1f96d",
|
||||
"1f34e", "1f34f", "1f350", "1f351", "1f352", "1f353", "1fad0", "1f95d",
|
||||
"1f345", "1fad2", "1f965", "1f951", "1f346", "1f954", "1f955", "1f33d",
|
||||
"1f336-fe0f", "1fad1", "1f952", "1f96c", "1f966", "1f9c4", "1f9c5", "1f344",
|
||||
"1f95c", "1f330", "1f35e", "1f950", "1f956", "1fad3", "1f968", "1f96f"
|
||||
),
|
||||
"Travel & Places" to listOf(
|
||||
"1f697", "1f695", "1f699", "1f68c", "1f68e", "1f3ce-fe0f", "1f693", "1f691",
|
||||
"1f692", "1f690", "1f69b", "1f69c", "1f6f5", "1f3cd-fe0f", "1f6b2", "1f6f4",
|
||||
"1f6f9", "1f6fc", "1f68f", "1f6e3-fe0f", "1f6e4-fe0f", "26fd", "1f6a8", "1f6a5",
|
||||
"1f6a6", "1f6d1", "1f6a7", "2708-fe0f", "1f6eb", "1f6ec", "1f6e9-fe0f", "1f4ba",
|
||||
"1f681", "1f69f", "1f6a0", "1f6a1", "1f6f0-fe0f", "1f680", "1f6f8", "1f6f6"
|
||||
),
|
||||
"Activities" to listOf(
|
||||
"26bd", "1f3c0", "1f3c8", "26be", "1f94e", "1f3be", "1f3d0", "1f3c9",
|
||||
"1f94f", "1f3b1", "1f3d3", "1f3f8", "1f3d2", "1f3d1", "1f94d", "1f3cf",
|
||||
"1f945", "26f3", "1f3bf", "1f6f7", "1f3af", "1fa80", "1fa81", "1f3b3",
|
||||
"1f3ae", "1f3b2", "1f9e9", "1f3ad", "1f3a8", "1f9f5", "1f9f6", "1f3bc",
|
||||
"1f3b5", "1f3b6", "1f399-fe0f", "1f39a-fe0f", "1f39b-fe0f", "1f3a4", "1f3a7", "1f4fb"
|
||||
),
|
||||
"Objects" to listOf(
|
||||
"1f4a1", "1f526", "1f3ee", "1fa94", "1f4d4", "1f4d5", "1f4d6", "1f4d7",
|
||||
"1f4d8", "1f4d9", "1f4da", "1f4d3", "1f4d2", "1f4c3", "1f4dc", "1f4c4",
|
||||
"1f4f0", "1f5de-fe0f", "1f4d1", "1f516", "1f3f7-fe0f", "1f4b0", "1fa99", "1f4b4",
|
||||
"1f4b5", "1f4b6", "1f4b7", "1f4b8", "1f4b3", "1f9fe", "1f4b9", "1f4e7",
|
||||
"1f4e8", "1f4e9", "1f4e4", "1f4e5", "1f4e6", "1f4eb", "1f4ea", "1f4ec"
|
||||
),
|
||||
"Symbols" to listOf(
|
||||
"2764-fe0f", "1f9e1", "1f49b", "1f49a", "1f499", "1f49c", "1f5a4", "1f90d",
|
||||
"1f90e", "1f494", "2763-fe0f", "1f495", "1f49e", "1f493", "1f497", "1f496",
|
||||
"1f498", "1f49d", "1f49f", "2665-fe0f", "1f4af", "1f4a2", "1f4a5", "1f4ab",
|
||||
"1f4a6", "1f4a8", "1f573-fe0f", "1f4ac", "1f441-fe0f-200d-1f5e8-fe0f", "1f5e8-fe0f",
|
||||
"1f5ef-fe0f", "1f4ad", "1f4a4", "1f44b", "1f91a", "1f590-fe0f", "270b", "1f596"
|
||||
),
|
||||
"Flags" to listOf(
|
||||
"1f3f3-fe0f", "1f3f4", "1f3c1", "1f6a9", "1f38c", "1f3f4-200d-2620-fe0f",
|
||||
"1f1e6-1f1e8", "1f1e6-1f1e9", "1f1e6-1f1ea", "1f1e6-1f1eb", "1f1e6-1f1ec",
|
||||
"1f1e6-1f1ee", "1f1e6-1f1f1", "1f1e6-1f1f2", "1f1e6-1f1f4", "1f1e6-1f1f6",
|
||||
"1f1e6-1f1f7", "1f1e6-1f1f8", "1f1e6-1f1f9", "1f1e6-1f1fa", "1f1e6-1f1fc",
|
||||
"1f1e6-1f1fd", "1f1e6-1f1ff", "1f1e7-1f1e6", "1f1e7-1f1e7", "1f1e7-1f1e9"
|
||||
)
|
||||
)
|
||||
/**
|
||||
* Проверяет, попадает ли emoji в диапазон категории
|
||||
*/
|
||||
fun emojiMatchesCategory(emoji: String, category: EmojiCategory): Boolean {
|
||||
val unified = emoji.lowercase().split("-").firstOrNull() ?: return false
|
||||
val codePoint = try { unified.toInt(16) } catch (e: Exception) { return false }
|
||||
return category.ranges.any { (start, end) -> codePoint in start..end }
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает все эмодзи из assets/emoji
|
||||
*/
|
||||
fun loadAllEmojisFromAssets(context: Context): List<String> {
|
||||
return try {
|
||||
context.assets.list("emoji")
|
||||
?.filter { it.endsWith(".png") }
|
||||
?.map { it.removeSuffix(".png") }
|
||||
?.sorted()
|
||||
?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Группирует эмодзи по категориям на основе Unicode диапазонов
|
||||
*/
|
||||
fun groupEmojisByCategory(allEmojis: List<String>): Map<String, List<String>> {
|
||||
val result = mutableMapOf<String, MutableList<String>>()
|
||||
val usedEmojis = mutableSetOf<String>()
|
||||
|
||||
// Инициализируем категории
|
||||
EMOJI_CATEGORIES.forEach { category ->
|
||||
result[category.key] = mutableListOf()
|
||||
}
|
||||
|
||||
// Группируем по Unicode диапазонам
|
||||
for (emoji in allEmojis) {
|
||||
for (category in EMOJI_CATEGORIES) {
|
||||
if (emojiMatchesCategory(emoji, category) && emoji !in usedEmojis) {
|
||||
result[category.key]?.add(emoji)
|
||||
usedEmojis.add(emoji)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем неклассифицированные в Symbols
|
||||
for (emoji in allEmojis) {
|
||||
if (emoji !in usedEmojis) {
|
||||
result["Symbols"]?.add(emoji)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертирует unified код в emoji символ
|
||||
@@ -271,6 +316,7 @@ fun CategoryButton(
|
||||
|
||||
/**
|
||||
* Apple Emoji Picker Panel - Liquid Glass стиль
|
||||
* Загружает ВСЕ эмодзи из assets
|
||||
*/
|
||||
@Composable
|
||||
fun AppleEmojiPickerPanel(
|
||||
@@ -279,12 +325,23 @@ fun AppleEmojiPickerPanel(
|
||||
onClose: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var selectedCategory by remember { mutableStateOf(EMOJI_CATEGORIES[0]) }
|
||||
val gridState = rememberLazyGridState()
|
||||
|
||||
// Мемоизируем текущие эмодзи
|
||||
val currentEmojis = remember(selectedCategory.key) {
|
||||
EMOJIS_BY_CATEGORY[selectedCategory.key] ?: emptyList()
|
||||
// Загружаем ВСЕ эмодзи из assets один раз
|
||||
val allEmojis = remember {
|
||||
loadAllEmojisFromAssets(context)
|
||||
}
|
||||
|
||||
// Группируем по категориям
|
||||
val emojisByCategory = remember(allEmojis) {
|
||||
groupEmojisByCategory(allEmojis)
|
||||
}
|
||||
|
||||
// Текущие эмодзи для выбранной категории
|
||||
val currentEmojis = remember(selectedCategory.key, emojisByCategory) {
|
||||
emojisByCategory[selectedCategory.key] ?: emptyList()
|
||||
}
|
||||
|
||||
// Сбрасываем скролл при смене категории
|
||||
|
||||
Reference in New Issue
Block a user