feat(emoji-picker): search
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
922d1da6c2
commit
49a99104cb
|
|
@ -12,6 +12,7 @@ import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
|
@ -20,6 +21,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.requiredSize
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||||
|
|
@ -29,8 +31,10 @@ import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowLeft
|
import androidx.compose.material.icons.filled.KeyboardArrowLeft
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowRight
|
import androidx.compose.material.icons.filled.KeyboardArrowRight
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
|
@ -41,6 +45,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
|
@ -144,13 +149,51 @@ fun EmojiPicker(
|
||||||
.first { it.character == "\uD83E\uDEF0" }
|
.first { it.character == "\uD83E\uDEF0" }
|
||||||
}
|
}
|
||||||
|
|
||||||
var searchQuery by remember { mutableStateOf("Search not implemented yet") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
val searchFieldOpacity by animateFloatAsState(
|
val searchFieldOpacity by animateFloatAsState(
|
||||||
if (showSkinToneMenu) 0f else 1f,
|
if (showSkinToneMenu) 0f else 1f,
|
||||||
animationSpec = RevoltTweenFloat,
|
animationSpec = RevoltTweenFloat,
|
||||||
label = "searchFieldOpacity"
|
label = "searchFieldOpacity"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val searchResults = remember { mutableStateListOf<EmojiPickerItem>() }
|
||||||
|
LaunchedEffect(searchQuery) {
|
||||||
|
searchResults.clear()
|
||||||
|
if (searchQuery.isBlank()) return@LaunchedEffect
|
||||||
|
searchResults.addAll(emojiImpl.searchForEmoji(searchQuery))
|
||||||
|
gridState.scrollToItem(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
val onServerEmoteInfo: (String) -> Unit = {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
|
scope.launch {
|
||||||
|
ActionChannel.send(
|
||||||
|
Action.EmoteInfo(
|
||||||
|
it
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onEmojiClick: (EmojiPickerItem) -> Unit = {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
|
when (it) {
|
||||||
|
is EmojiPickerItem.UnicodeEmoji -> onEmojiSelected(
|
||||||
|
emojiImpl.applyFitzpatrickSkinTone(
|
||||||
|
it,
|
||||||
|
currentSkinTone
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
is EmojiPickerItem.ServerEmote -> onEmojiSelected(":${it.emote.id}:")
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val clearQueryButtonOpacity = animateFloatAsState(
|
||||||
|
if (searchQuery.isNotEmpty()) 1f else 0f,
|
||||||
|
animationSpec = RevoltTweenFloat,
|
||||||
|
label = "clearQueryButtonOpacity"
|
||||||
|
)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -178,9 +221,27 @@ fun EmojiPicker(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(MaterialTheme.shapes.small)
|
.clip(MaterialTheme.shapes.small)
|
||||||
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp))
|
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp))
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
) {
|
) {
|
||||||
innerTextField()
|
innerTextField()
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = stringResource(R.string.emoji_picker_clear_search),
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.padding(4.dp)
|
||||||
|
.size(24.dp)
|
||||||
|
.then(
|
||||||
|
if (searchQuery.isNotEmpty()) Modifier.clickable {
|
||||||
|
searchQuery = ""
|
||||||
|
focusManager.clearFocus() // this prevents the text field Z-below from gaining focus
|
||||||
|
} else Modifier
|
||||||
|
)
|
||||||
|
.align(Alignment.CenterEnd)
|
||||||
|
.alpha(clearQueryButtonOpacity.value)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -273,100 +334,103 @@ fun EmojiPicker(
|
||||||
|
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(4.dp))
|
||||||
|
|
||||||
Row(
|
AnimatedVisibility(searchResults.isEmpty()) {
|
||||||
modifier = Modifier
|
Row(
|
||||||
.horizontalScroll(categoryRowScrollState)
|
modifier = Modifier
|
||||||
.height(37.dp),
|
.horizontalScroll(categoryRowScrollState)
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
.height(37.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
servers.forEach { server ->
|
) {
|
||||||
Column(
|
servers.forEach { server ->
|
||||||
modifier = Modifier
|
Column(
|
||||||
.clip(CircleShape)
|
modifier = Modifier
|
||||||
.clickable {
|
.clip(CircleShape)
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
.clickable {
|
||||||
scope.launch {
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
val index =
|
scope.launch {
|
||||||
pickerList.indexOfFirst { it is EmojiPickerItem.Section && it.category is Category.ServerEmoteCategory && it.category.server == server }
|
val index =
|
||||||
gridState.animateScrollToItem(index)
|
pickerList.indexOfFirst { it is EmojiPickerItem.Section && it.category is Category.ServerEmoteCategory && it.category.server == server }
|
||||||
|
gridState.animateScrollToItem(index)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.then(
|
||||||
|
if (currentCategory.value is Category.ServerEmoteCategory && (currentCategory.value as Category.ServerEmoteCategory).server == server) {
|
||||||
|
Modifier.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f))
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.padding(4.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
if (server.icon == null) {
|
||||||
|
IconPlaceholder(
|
||||||
|
name = server.name ?: stringResource(R.string.unknown),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.fillMaxSize()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
RemoteImage(
|
||||||
|
url = "$REVOLT_FILES/icons/${server.icon.id}/icon.gif?max_side=64",
|
||||||
|
description = server.name,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.fillMaxSize()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.then(
|
}
|
||||||
if (currentCategory.value is Category.ServerEmoteCategory && (currentCategory.value as Category.ServerEmoteCategory).server == server) {
|
}
|
||||||
Modifier.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f))
|
UnicodeEmojiSection.entries.forEach { category ->
|
||||||
} else {
|
Column(
|
||||||
Modifier
|
modifier = Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
|
scope.launch {
|
||||||
|
val index =
|
||||||
|
pickerList.indexOfFirst { it is EmojiPickerItem.Section && it.category is Category.UnicodeEmojiCategory && it.category.definition == category }
|
||||||
|
gridState.animateScrollToItem(index)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
.then(
|
||||||
.aspectRatio(1f)
|
if (currentCategory.value is Category.UnicodeEmojiCategory && (currentCategory.value as Category.UnicodeEmojiCategory).definition == category) {
|
||||||
.padding(4.dp),
|
Modifier.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f))
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
} else {
|
||||||
verticalArrangement = Arrangement.Center,
|
Modifier
|
||||||
) {
|
}
|
||||||
if (server.icon == null) {
|
)
|
||||||
IconPlaceholder(
|
.aspectRatio(1f)
|
||||||
name = server.name ?: stringResource(R.string.unknown),
|
.padding(4.dp),
|
||||||
fontSize = 16.sp,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier
|
verticalArrangement = Arrangement.Center,
|
||||||
.clip(CircleShape)
|
) {
|
||||||
.fillMaxSize()
|
Icon(
|
||||||
)
|
painter = when (category) {
|
||||||
} else {
|
UnicodeEmojiSection.Smileys -> painterResource(R.drawable.ic_emoticon_24dp)
|
||||||
RemoteImage(
|
UnicodeEmojiSection.People -> painterResource(R.drawable.ic_human_greeting_variant_24dp)
|
||||||
url = "$REVOLT_FILES/icons/${server.icon.id}/icon.gif?max_side=64",
|
UnicodeEmojiSection.Animals -> painterResource(R.drawable.ic_snake_24dp)
|
||||||
description = server.name,
|
UnicodeEmojiSection.Food -> painterResource(R.drawable.ic_glass_mug_variant_24dp)
|
||||||
modifier = Modifier
|
UnicodeEmojiSection.Travel -> painterResource(R.drawable.ic_train_bus_24dp)
|
||||||
.clip(CircleShape)
|
UnicodeEmojiSection.Activities -> painterResource(R.drawable.ic_skate_24dp)
|
||||||
.fillMaxSize()
|
UnicodeEmojiSection.Objects -> painterResource(R.drawable.ic_table_chair_24dp)
|
||||||
|
UnicodeEmojiSection.Symbols -> painterResource(R.drawable.ic_symbol_24dp)
|
||||||
|
UnicodeEmojiSection.Flags -> painterResource(R.drawable.ic_flag_24dp)
|
||||||
|
},
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (currentCategory.value is Category.UnicodeEmojiCategory && (currentCategory.value as Category.UnicodeEmojiCategory).definition == category) {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
} else LocalContentColor.current
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UnicodeEmojiSection.entries.forEach { category ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(CircleShape)
|
|
||||||
.clickable {
|
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
||||||
scope.launch {
|
|
||||||
val index =
|
|
||||||
pickerList.indexOfFirst { it is EmojiPickerItem.Section && it.category is Category.UnicodeEmojiCategory && it.category.definition == category }
|
|
||||||
gridState.animateScrollToItem(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.then(
|
|
||||||
if (currentCategory.value is Category.UnicodeEmojiCategory && (currentCategory.value as Category.UnicodeEmojiCategory).definition == category) {
|
|
||||||
Modifier.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f))
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.aspectRatio(1f)
|
|
||||||
.padding(4.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = when (category) {
|
|
||||||
UnicodeEmojiSection.Smileys -> painterResource(R.drawable.ic_emoticon_24dp)
|
|
||||||
UnicodeEmojiSection.People -> painterResource(R.drawable.ic_human_greeting_variant_24dp)
|
|
||||||
UnicodeEmojiSection.Animals -> painterResource(R.drawable.ic_snake_24dp)
|
|
||||||
UnicodeEmojiSection.Food -> painterResource(R.drawable.ic_glass_mug_variant_24dp)
|
|
||||||
UnicodeEmojiSection.Travel -> painterResource(R.drawable.ic_train_bus_24dp)
|
|
||||||
UnicodeEmojiSection.Activities -> painterResource(R.drawable.ic_skate_24dp)
|
|
||||||
UnicodeEmojiSection.Objects -> painterResource(R.drawable.ic_table_chair_24dp)
|
|
||||||
UnicodeEmojiSection.Symbols -> painterResource(R.drawable.ic_symbol_24dp)
|
|
||||||
UnicodeEmojiSection.Flags -> painterResource(R.drawable.ic_flag_24dp)
|
|
||||||
},
|
|
||||||
contentDescription = null,
|
|
||||||
tint = if (currentCategory.value is Category.UnicodeEmojiCategory && (currentCategory.value as Category.UnicodeEmojiCategory).definition == category) {
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
} else LocalContentColor.current
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
state = gridState,
|
state = gridState,
|
||||||
columns = GridCells.Fixed(spanCount),
|
columns = GridCells.Fixed(spanCount),
|
||||||
|
|
@ -374,8 +438,67 @@ fun EmojiPicker(
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
|
if (searchResults.isNotEmpty()) {
|
||||||
|
item(
|
||||||
|
key = "searchResultsHeader",
|
||||||
|
span = {
|
||||||
|
GridItemSpan(spanCount)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.emoji_picker_search_results_header),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search results do not get a key, this is intentional.
|
||||||
|
items(
|
||||||
|
searchResults.size,
|
||||||
|
span = {
|
||||||
|
val item = searchResults[it]
|
||||||
|
when (item) {
|
||||||
|
is EmojiPickerItem.UnicodeEmoji -> GridItemSpan(1)
|
||||||
|
is EmojiPickerItem.ServerEmote -> GridItemSpan(1)
|
||||||
|
is EmojiPickerItem.Section -> GridItemSpan(spanCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { index ->
|
||||||
|
PickerItem(
|
||||||
|
item = searchResults[index],
|
||||||
|
skinToneFactory = { emojiImpl.applyFitzpatrickSkinTone(it, currentSkinTone) },
|
||||||
|
onClick = onEmojiClick,
|
||||||
|
onServerEmoteInfo = onServerEmoteInfo,
|
||||||
|
lesserHeaders = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchResults.isNotEmpty()) {
|
||||||
|
item(
|
||||||
|
key = "searchResultsFooter",
|
||||||
|
span = {
|
||||||
|
GridItemSpan(spanCount)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
items(
|
items(
|
||||||
pickerList.size,
|
pickerList.size,
|
||||||
|
key = {
|
||||||
|
when (val item = pickerList[it]) {
|
||||||
|
is EmojiPickerItem.UnicodeEmoji -> item.character
|
||||||
|
is EmojiPickerItem.ServerEmote -> item.emote.id ?: it
|
||||||
|
is EmojiPickerItem.Section -> when (val category = item.category) {
|
||||||
|
is Category.UnicodeEmojiCategory -> category.definition.googleName
|
||||||
|
is Category.ServerEmoteCategory -> category.server.id ?: it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
span = {
|
span = {
|
||||||
val item = pickerList[it]
|
val item = pickerList[it]
|
||||||
when (item) {
|
when (item) {
|
||||||
|
|
@ -385,88 +508,88 @@ fun EmojiPicker(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { index ->
|
) { index ->
|
||||||
when (val item = pickerList[index]) {
|
PickerItem(
|
||||||
is EmojiPickerItem.UnicodeEmoji -> {
|
item = pickerList[index],
|
||||||
Column(
|
skinToneFactory = { emojiImpl.applyFitzpatrickSkinTone(it, currentSkinTone) },
|
||||||
modifier = Modifier
|
onClick = onEmojiClick,
|
||||||
.clip(CircleShape)
|
onServerEmoteInfo = onServerEmoteInfo
|
||||||
.clickable {
|
)
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
||||||
onEmojiSelected(
|
|
||||||
emojiImpl.applyFitzpatrickSkinTone(
|
|
||||||
item,
|
|
||||||
currentSkinTone
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.aspectRatio(1f)
|
|
||||||
.weight(1f),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
emojiImpl.applyFitzpatrickSkinTone(
|
|
||||||
item,
|
|
||||||
currentSkinTone
|
|
||||||
),
|
|
||||||
style = LocalTextStyle.current.copy(fontSize = 20.sp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is EmojiPickerItem.ServerEmote -> {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(CircleShape)
|
|
||||||
.combinedClickable(
|
|
||||||
onClick = {
|
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
||||||
onEmojiSelected(":${item.emote.id}:")
|
|
||||||
},
|
|
||||||
onLongClick = {
|
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
|
||||||
scope.launch {
|
|
||||||
item.emote.id?.let {
|
|
||||||
ActionChannel.send(
|
|
||||||
Action.EmoteInfo(
|
|
||||||
it
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.aspectRatio(1f)
|
|
||||||
.weight(1f),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
RemoteImage(
|
|
||||||
url = "$REVOLT_FILES/emojis/${item.emote.id}/emoji.gif",
|
|
||||||
description = item.emote.name,
|
|
||||||
contentScale = ContentScale.Fit,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is EmojiPickerItem.Section -> {
|
|
||||||
Text(
|
|
||||||
when (item.category) {
|
|
||||||
is Category.UnicodeEmojiCategory -> stringResource(item.category.definition.nameResource)
|
|
||||||
is Category.ServerEmoteCategory -> item.category.server.name
|
|
||||||
?: stringResource(R.string.unknown)
|
|
||||||
},
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ColumnScope.PickerItem(
|
||||||
|
item: EmojiPickerItem,
|
||||||
|
skinToneFactory: (EmojiPickerItem.UnicodeEmoji) -> String,
|
||||||
|
onClick: (EmojiPickerItem) -> Unit,
|
||||||
|
onServerEmoteInfo: (String) -> Unit,
|
||||||
|
lesserHeaders: Boolean = false,
|
||||||
|
) {
|
||||||
|
when (item) {
|
||||||
|
is EmojiPickerItem.UnicodeEmoji -> {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable {
|
||||||
|
}
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.weight(1f),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = skinToneFactory(item),
|
||||||
|
style = LocalTextStyle.current.copy(fontSize = 20.sp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is EmojiPickerItem.ServerEmote -> {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = { onClick(item) },
|
||||||
|
onLongClick = { item.emote.id?.let { onServerEmoteInfo(it) } }
|
||||||
|
)
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.weight(1f),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
RemoteImage(
|
||||||
|
url = "$REVOLT_FILES/emojis/${item.emote.id}/emoji.gif",
|
||||||
|
description = item.emote.name,
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is EmojiPickerItem.Section -> {
|
||||||
|
Text(
|
||||||
|
when (item.category) {
|
||||||
|
is Category.UnicodeEmojiCategory -> stringResource(item.category.definition.nameResource)
|
||||||
|
is Category.ServerEmoteCategory -> item.category.server.name
|
||||||
|
?: stringResource(R.string.unknown)
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
.then(
|
||||||
|
if (lesserHeaders) {
|
||||||
|
Modifier.alpha(.7f)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,53 @@ class EmojiImpl {
|
||||||
?: item.character
|
?: item.character
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a search on the flat picker list to find all custom and unicode emoji that match the
|
||||||
|
* query.
|
||||||
|
*/
|
||||||
|
fun searchForEmoji(query: String): List<EmojiPickerItem> {
|
||||||
|
val list = mutableListOf<EmojiPickerItem>()
|
||||||
|
|
||||||
|
for (server in serversWithEmotes()) {
|
||||||
|
val emotes = RevoltAPI.emojiCache.values.filter { it.parent?.id == server.id }
|
||||||
|
val matchingEmotes =
|
||||||
|
emotes.filter { it.name?.contains(query, ignoreCase = true) ?: false }
|
||||||
|
if (matchingEmotes.isNotEmpty()) {
|
||||||
|
list.add(EmojiPickerItem.Section(Category.ServerEmoteCategory(server)))
|
||||||
|
list.addAll(matchingEmotes.map { EmojiPickerItem.ServerEmote(it) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (group in metadata) {
|
||||||
|
val matchingEmoji = group.emoji.filter {
|
||||||
|
it.shortcodes.any { code ->
|
||||||
|
code.contains(
|
||||||
|
query,
|
||||||
|
ignoreCase = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matchingEmoji.isNotEmpty()) {
|
||||||
|
val category =
|
||||||
|
UnicodeEmojiSection.entries.find { it.googleName == group.group } ?: continue
|
||||||
|
list.add(EmojiPickerItem.Section(Category.UnicodeEmojiCategory(category)))
|
||||||
|
list.addAll(matchingEmoji.map { emoji ->
|
||||||
|
EmojiPickerItem.UnicodeEmoji(
|
||||||
|
emoji.base.joinToString("") { String(Character.toChars(it.toInt())) },
|
||||||
|
emoji.alternates.any { alternate ->
|
||||||
|
alternate.any { codepoint ->
|
||||||
|
codepoint in 0x1F3FB..0x1F3FF
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emoji.alternates
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
metadata = initMetadata(RevoltApplication.instance.applicationContext)
|
metadata = initMetadata(RevoltApplication.instance.applicationContext)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -350,6 +350,8 @@
|
||||||
<string name="emoji_picker_skin_tone_fitzpatrick_4">Medium skin tone</string>
|
<string name="emoji_picker_skin_tone_fitzpatrick_4">Medium skin tone</string>
|
||||||
<string name="emoji_picker_skin_tone_fitzpatrick_5">Medium-dark skin tone</string>
|
<string name="emoji_picker_skin_tone_fitzpatrick_5">Medium-dark skin tone</string>
|
||||||
<string name="emoji_picker_skin_tone_fitzpatrick_6">Dark skin tone</string>
|
<string name="emoji_picker_skin_tone_fitzpatrick_6">Dark skin tone</string>
|
||||||
|
<string name="emoji_picker_search_results_header">Search results</string>
|
||||||
|
<string name="emoji_picker_clear_search">Clear search</string>
|
||||||
|
|
||||||
<string name="spark_sidebar_settings_tutorial">The settings are in the sidebar</string>
|
<string name="spark_sidebar_settings_tutorial">The settings are in the sidebar</string>
|
||||||
<string name="spark_sidebar_settings_tutorial_description_1">You can open the sidebar by swiping from the left edge of the screen.</string>
|
<string name="spark_sidebar_settings_tutorial_description_1">You can open the sidebar by swiping from the left edge of the screen.</string>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue