diff --git a/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt b/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt index 25663f03..792e1920 100644 --- a/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt +++ b/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt @@ -347,7 +347,7 @@ class KeyHandler( if (!ime.returnIsSubsequentRequired()) { ime.handleConjugateKeys(code, false) ime.moveToIdleState() - ime.saveConjugateModeType(language, isSubsequentArea = false) + ime.saveConjugateModeType("none") } else { val word = ime.handleConjugateKeys(code, true) ime.setupConjugateSubView(ime.returnSubsequentData(), word) diff --git a/app/src/keyboards/java/be/scri/helpers/KeyboardLanguageMappingConstants.kt b/app/src/keyboards/java/be/scri/helpers/KeyboardLanguageMappingConstants.kt index 72ffd571..24c11805 100644 --- a/app/src/keyboards/java/be/scri/helpers/KeyboardLanguageMappingConstants.kt +++ b/app/src/keyboards/java/be/scri/helpers/KeyboardLanguageMappingConstants.kt @@ -93,6 +93,15 @@ object KeyboardLanguageMappingConstants { "objects" to ENInterfaceVariables.OBJECTS_EMOJI_HEADER, "symbols" to ENInterfaceVariables.SYMBOLS_EMOJI_HEADER, "flags" to ENInterfaceVariables.FLAGS_EMOJI_HEADER, + "kaomoji_recent" to ENInterfaceVariables.KAOMOJI_RECENT_HEADER, + "kaomoji_joy" to ENInterfaceVariables.KAOMOJI_JOY_HEADER, + "kaomoji_love" to ENInterfaceVariables.KAOMOJI_LOVE_HEADER, + "kaomoji_cool" to ENInterfaceVariables.KAOMOJI_COOL_HEADER, + "kaomoji_sad" to ENInterfaceVariables.KAOMOJI_SAD_HEADER, + "kaomoji_angry" to ENInterfaceVariables.KAOMOJI_ANGRY_HEADER, + "kaomoji_surprised" to ENInterfaceVariables.KAOMOJI_SURPRISED_HEADER, + "kaomoji_shrug" to ENInterfaceVariables.KAOMOJI_SHRUG_HEADER, + "kaomoji_animals" to ENInterfaceVariables.KAOMOJI_ANIMALS_HEADER, ), "ES" to mapOf( diff --git a/app/src/keyboards/java/be/scri/helpers/english/ENInterfaceVariables.kt b/app/src/keyboards/java/be/scri/helpers/english/ENInterfaceVariables.kt index dca8f86f..384dbac9 100644 --- a/app/src/keyboards/java/be/scri/helpers/english/ENInterfaceVariables.kt +++ b/app/src/keyboards/java/be/scri/helpers/english/ENInterfaceVariables.kt @@ -90,4 +90,14 @@ object ENInterfaceVariables { const val OBJECTS_EMOJI_HEADER = "Objects" const val SYMBOLS_EMOJI_HEADER = "Symbols" const val FLAGS_EMOJI_HEADER = "Flags" + + const val KAOMOJI_RECENT_HEADER = "Recent" + const val KAOMOJI_JOY_HEADER = "Joy" + const val KAOMOJI_LOVE_HEADER = "Love" + const val KAOMOJI_COOL_HEADER = "Cool" + const val KAOMOJI_SAD_HEADER = "Sad" + const val KAOMOJI_ANGRY_HEADER = "Angry" + const val KAOMOJI_SURPRISED_HEADER = "Surprised" + const val KAOMOJI_SHRUG_HEADER = "Shrug" + const val KAOMOJI_ANIMALS_HEADER = "Animals" } diff --git a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt index 4fe40d65..667f5519 100644 --- a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt +++ b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt @@ -28,6 +28,7 @@ import be.scri.helpers.AutoGridLayoutManager import be.scri.helpers.EMOJI_SPEC_FILE_PATH import be.scri.helpers.EmojiAdapter import be.scri.helpers.EmojiData +import be.scri.helpers.KaomojiData import be.scri.helpers.KeyboardBase import be.scri.helpers.KeyboardLanguageMappingConstants.conjugatePlaceholder import be.scri.helpers.KeyboardLanguageMappingConstants.emojiCategoryHeaders @@ -38,11 +39,17 @@ import be.scri.helpers.PreferencesHelper import be.scri.helpers.PreferencesHelper.getIsDarkModeOrNot import be.scri.helpers.english.ENInterfaceVariables.ALREADY_PLURAL_MSG import be.scri.helpers.getCategoryIconRes +import be.scri.helpers.getRecentKaomojis import be.scri.helpers.parseRawEmojiSpecsFile +import be.scri.helpers.parseRawKaomojiSpecsFile +import be.scri.helpers.saveRecentKaomoji import be.scri.models.ScribeState import be.scri.services.GeneralKeyboardIME import be.scri.views.KeyboardView +private const val PALETTE_MODE_EMOJI = 0 +private const val PALETTE_MODE_KAOMOJI = 1 + /** * Manages the UI elements and state transitions for the GeneralKeyboardIME. * This class handles View interactions, visibility toggling, and layout updates. @@ -854,6 +861,7 @@ class KeyboardUIManager( Thread { val fullEmojiList = parseRawEmojiSpecsFile(context, EMOJI_SPEC_FILE_PATH) + val kaomojisList = parseRawKaomojiSpecsFile(context) val systemFontPaint = android.graphics.Paint().apply { typeface = android.graphics.Typeface.DEFAULT @@ -864,15 +872,64 @@ class KeyboardUIManager( } android.os.Handler(android.os.Looper.getMainLooper()).post { - setupEmojiAdapter(emojis, language) + setupPaletteModeTabs(emojis, kaomojisList, language) } }.start() } + private var activePaletteMode = PALETTE_MODE_EMOJI + /** - * Sets up the emoji RecyclerView adapter and category strip. - * - * @param emojis The filtered list of emojis the device can render. + * Handles toggle switching between standard Emojis and Kaomojis via top_bar_emoji_toggle and top_bar_kaomoji_toggle. + */ + private fun setupPaletteModeTabs( + emojis: List, + kaomojis: List, + language: String, + ) { + fun updateToggleState() { + val isDarkMode = getIsDarkModeOrNot(context) + if (activePaletteMode == PALETTE_MODE_EMOJI) { + binding.topBarEmojiToggle.isSelected = true + binding.topBarEmojiToggle.imageTintList = ColorStateList.valueOf(Color.WHITE) + + binding.topBarKaomojiToggle.isSelected = false + binding.topBarKaomojiToggle.setTextColor(if (isDarkMode) Color.LTGRAY else Color.DKGRAY) + } else { + binding.topBarKaomojiToggle.isSelected = true + binding.topBarKaomojiToggle.setTextColor(Color.WHITE) + + binding.topBarEmojiToggle.isSelected = false + binding.topBarEmojiToggle.imageTintList = ColorStateList.valueOf(if (isDarkMode) Color.LTGRAY else Color.DKGRAY) + } + } + + binding.topBarEmojiToggle.setOnClickListener { + if (activePaletteMode != PALETTE_MODE_EMOJI) { + activePaletteMode = PALETTE_MODE_EMOJI + updateToggleState() + setupEmojiAdapter(emojis, language) + } + } + + binding.topBarKaomojiToggle.setOnClickListener { + if (activePaletteMode != PALETTE_MODE_KAOMOJI) { + activePaletteMode = PALETTE_MODE_KAOMOJI + updateToggleState() + setupKaomojiAdapter(kaomojis, language) + } + } + + updateToggleState() + if (activePaletteMode == PALETTE_MODE_EMOJI) { + setupEmojiAdapter(emojis, language) + } else { + setupKaomojiAdapter(kaomojis, language) + } + } + + /** + * Sets up the emoji RecyclerView adapter and category strip for standard emojis. */ private fun setupEmojiAdapter( emojis: List, @@ -889,7 +946,7 @@ class KeyboardUIManager( emojiLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int = - if (emojiItems[position] is EmojiAdapter.Item.Category) { + if (emojiItems.getOrNull(position) is EmojiAdapter.Item.Category) { emojiLayoutManager.spanCount } else { 1 @@ -898,46 +955,91 @@ class KeyboardUIManager( binding.emojisList.layoutManager = emojiLayoutManager binding.emojisList.adapter = - EmojiAdapter(context, emojiItems, categoryHeaders) { emojiData -> - listener.onEmojiSelected(emojiData.emoji) - } + EmojiAdapter( + context = context, + items = emojiItems, + categoryHeaders = categoryHeaders, + itemClick = { emojiData -> + listener.onEmojiSelected(emojiData.emoji) + }, + ) - setupEmojiCategoryStrip(emojiCategories, emojiItems, emojiLayoutManager) + setupEmojiCategoryIconStrip(emojiCategories.keys, emojiItems, emojiLayoutManager) } /** - * Groups emojis by category. - * - * @param emojis The full list of emojis. - * @return A map of category name to list of emojis in corresponding category. + * Sets up the kaomoji RecyclerView adapter and category strip for Kaomojis mode. */ - private fun prepareEmojiCategories(emojis: List): Map> = emojis.groupBy { it.category } + private fun setupKaomojiAdapter( + kaomojis: List, + language: String, + ) { + val recentKaomojis = getRecentKaomojis(context) + val kaomojiCategories = mutableMapOf>() + if (recentKaomojis.isNotEmpty()) { + kaomojiCategories["kaomoji_recent"] = recentKaomojis + } + kaomojiCategories.putAll(prepareKaomojiCategories(kaomojis)) + + val categoryHeaders = + (emojiCategoryHeaders["EN"] ?: emptyMap()) + (emojiCategoryHeaders[getLanguageAlias(language)] ?: emptyMap()) + + val defaultCategoryKey = if (recentKaomojis.isNotEmpty()) "kaomoji_recent" else (kaomojiCategories.keys.firstOrNull() ?: "kaomoji_joy") + updateKaomojiGrid(defaultCategoryKey, kaomojiCategories, categoryHeaders) + setupKaomojiCategoryNameStrip(kaomojiCategories.keys, kaomojiCategories, categoryHeaders) + } /** - * Builds a list of category headers and emoji items for the RecyclerView. - * - * @param categories The map of categories to their emojis. - * @return A flat list of [EmojiAdapter.Item] objects. + * Updates the Kaomoji grid to show ONLY items from the selected category (no section titles). */ + private fun updateKaomojiGrid( + selectedCategoryKey: String, + kaomojiCategories: Map>, + categoryHeaders: Map, + ) { + val categoryKaomojis = kaomojiCategories[selectedCategoryKey] ?: emptyList() + val kaomojiItems = categoryKaomojis.map { EmojiAdapter.Item.Kaomoji(it) } + + val emojiItemSize = context.resources.getDimensionPixelSize(R.dimen.emoji_item_size) + val emojiLayoutManager = AutoGridLayoutManager(context, emojiItemSize) + + emojiLayoutManager.spanSizeLookup = + object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int = (emojiLayoutManager.spanCount / 2).coerceAtLeast(2) + } + + binding.emojisList.layoutManager = emojiLayoutManager + binding.emojisList.adapter = + EmojiAdapter( + context = context, + items = kaomojiItems, + categoryHeaders = categoryHeaders, + itemClick = {}, + kaomojiClick = { kaomojiData -> + saveRecentKaomoji(context, kaomojiData) + listener.onEmojiSelected(kaomojiData.kaomoji) + }, + ) + } + + private fun prepareEmojiCategories(emojis: List): Map> = emojis.groupBy { it.category } + + private fun prepareKaomojiCategories(kaomojis: List): Map> = kaomojis.groupBy { it.category } + private fun prepareEmojiItems(categories: Map>): List { - val emojiItems = mutableListOf() + val items = mutableListOf() categories.entries.forEach { (category, emojis) -> - emojiItems.add(EmojiAdapter.Item.Category(category)) - emojis.forEach { emojiItems.add(EmojiAdapter.Item.Emoji(it)) } + items.add(EmojiAdapter.Item.Category(category)) + emojis.forEach { items.add(EmojiAdapter.Item.Emoji(it)) } } - return emojiItems + return items } /** - * Populates the emoji category strip at the bottom of the palette. - * Tapping a category icon scrolls the emoji list to that category. - * - * @param categories The map of category names to their emojis. - * @param emojiItems The full flat list used to find category positions. - * @param layoutManager The AutoGridLayoutManager used to scroll to positions. + * Populates standard emoji category icons in the bottom strip. */ - private fun setupEmojiCategoryStrip( - categories: Map>, + private fun setupEmojiCategoryIconStrip( + categoryKeys: Set, emojiItems: List, layoutManager: AutoGridLayoutManager, ) { @@ -952,7 +1054,7 @@ class KeyboardUIManager( var activeButton: android.widget.ImageButton? = null - categories.keys.forEachIndexed { index, category -> + categoryKeys.forEachIndexed { index, category -> val button = android.widget.ImageButton(context).apply { setImageResource(getCategoryIconRes(category)) @@ -986,6 +1088,61 @@ class KeyboardUIManager( } } + /** + * Populates styled text chips with category names for Kaomojis mode in the bottom strip. + */ + private fun setupKaomojiCategoryNameStrip( + categoryKeys: Set, + kaomojiCategories: Map>, + categoryHeaders: Map, + ) { + binding.emojiCategoriesStrip.removeAllViews() + val isDarkMode = getIsDarkModeOrNot(context) + val inactiveTextColor = if (isDarkMode) Color.LTGRAY else Color.DKGRAY + + var activeChip: TextView? = null + + categoryKeys.forEachIndexed { index, categoryKey -> + val headerTitle = categoryHeaders[categoryKey] ?: categoryKey.removePrefix("kaomoji_").replaceFirstChar { it.uppercase() } + val chip = + TextView(context).apply { + text = headerTitle + textSize = 13f + setPadding(28, 12, 28, 12) + layoutParams = + android.widget.LinearLayout + .LayoutParams( + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, + ).apply { + setMargins(8, 0, 8, 0) + } + background = ContextCompat.getDrawable(context, R.drawable.kaomoji_chip_background) + isSelected = (index == 0) + setTextColor(if (isSelected) Color.WHITE else inactiveTextColor) + setTypeface(null, if (isSelected) android.graphics.Typeface.BOLD else android.graphics.Typeface.NORMAL) + + setOnClickListener { + if (activeChip != this) { + activeChip?.isSelected = false + activeChip?.setTextColor(inactiveTextColor) + activeChip?.setTypeface(null, android.graphics.Typeface.NORMAL) + + isSelected = true + setTextColor(Color.WHITE) + setTypeface(null, android.graphics.Typeface.BOLD) + activeChip = this + + updateKaomojiGrid(categoryKey, kaomojiCategories, categoryHeaders) + } + } + } + + if (index == 0) activeChip = chip + binding.emojiCategoriesStrip.addView(chip) + } + } + /** * Hides the emoji palette and restores the normal keyboard view and command options bar. * Called when the user taps the close button, the ABC button, or finishes emoji selection. diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt index c474e552..ecffb8c8 100644 --- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt +++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt @@ -755,12 +755,8 @@ abstract class GeneralKeyboardIME( * @param language The current keyboard language. * @param isSubsequentArea true if this is for a secondary view. */ - internal fun saveConjugateModeType( - language: String = this.language, - isSubsequentArea: Boolean = false, - ) { + internal fun saveConjugateModeType(mode: String = "none") { val sharedPref = applicationContext.getSharedPreferences("keyboard_preferences", MODE_PRIVATE) - val mode = if (!isSubsequentArea) defaultConjugateModeType else "none" sharedPref.edit { putString("conjugate_mode_type", mode) } } @@ -1032,7 +1028,7 @@ abstract class GeneralKeyboardIME( invalidCommandSource = ScribeState.CONJUGATE ScribeState.INVALID } else { - saveConjugateModeType(language) + saveConjugateModeType(defaultConjugateModeType) ScribeState.SELECT_VERB_CONJUNCTION } refreshUI() @@ -2024,7 +2020,7 @@ abstract class GeneralKeyboardIME( val uniqueData = data.distinct() val filteredData = uniqueData.filter { sublist -> sublist.contains(word) } val flattenList = filteredData.flatten() - saveConjugateModeType(language = language, true) + saveConjugateModeType("2x1") val prefs = applicationContext.getSharedPreferences("keyboard_preferences", MODE_PRIVATE) prefs.edit(commit = true) { putString("conjugate_mode_type", "2x1") } val keyboardXmlId = getKeyboardLayoutForState(currentState, true, flattenList.size) @@ -2066,7 +2062,7 @@ abstract class GeneralKeyboardIME( ): Int = when (state) { ScribeState.SELECT_VERB_CONJUNCTION -> { - saveConjugateModeType(language) + saveConjugateModeType(defaultConjugateModeType) if (!isSubsequentArea && dataSize == 0) { defaultConjugateLayoutXML } else { diff --git a/app/src/main/assets/kaomoji_spec.txt b/app/src/main/assets/kaomoji_spec.txt new file mode 100644 index 00000000..237f0fdc --- /dev/null +++ b/app/src/main/assets/kaomoji_spec.txt @@ -0,0 +1,114 @@ +# Kaomoji Specification File +[kaomoji_joy] +(•‿•);Happy;(•‿•) +(。◕‿◕。);Smile;(。◕‿◕。) +(*^ω^*);Joy;(*^ω^*) +(≧◡≦);Delight;(≧◡≦) +(─‿─);Satisfied;(─‿─) +(★^O^★);Excited;(★^O^★) +(•ө•);Cute;(•ө•) +(◠‿◠);Peaceful;(◠‿◠) +(◕‿◕✿);Flower;(◕‿◕✿) +(o^▽^o);Giggle;(o^▽^o) +(☆▽☆);Sparkle;(☆▽☆) +(⌒▽⌒);Cheerful;(⌒▽⌒) +\(^ヮ^)/;Yay;(^ヮ^) +( ̄▽ ̄);Relaxed;( ̄▽ ̄) +(っ˘▽˘)っ;Happy Hug;(っ˘▽˘)っ +\( ̄▽ ̄)/;Hurray;\( ̄▽ ̄)/ +(^▽^);Big Smile;(^▽^) +(*≧ω≦);Super Happy;(*≧ω≦) +(❁´◡`❁);Blushing Smile;(❁´◡`❁) +(๑˃̵ᴗ˂̵)و;Victory;(๑˃̵ᴗ˂̵)و + +[kaomoji_love] +(♡‿♡);Love Eyes;(♡‿♡) +(灬♥ω♥灬);In Love;(灬♥ω♥灬) +(*♡∀♡);Heart Eyes;(*♡∀♡) +(っ˘з(˘⌣˘ );Kiss;(っ˘з(˘⌣˘ ) +(◍•ᴗ•◍)❤;Cute Heart;(◍•ᴗ•◍)❤ +(づ ̄ ³ ̄)づ;Blow Kiss;(づ ̄ ³ ̄)づ +(っ°v°)っ;Hug;(っ°v°)っ +(♥_♥);Heart Face;(♥_♥) +(✿ ♥‿♥);Flower Love;(✿ ♥‿♥) +(๑ºvº๑);Loving;(๑ºvº๑) +(⁄ ⁄•⁄ω⁄•⁄ ⁄);Shy Love;(⁄ ⁄•⁄ω⁄•⁄ ⁄) +(˘∀˘)/(♥v♥);Romantic;(˘∀˘)/(♥v♥) + +[kaomoji_cool] +(⌐■_■);Sunglasses;(⌐■_■) +( ͡° ͜ʖ ͡°);Lenny;( ͡° ͜ʖ ͡°) +( ▀ ͜▄ ͡▀);Cool Shades;( ▀ ͜▄ ͡▀) +ᕙ( •̀ ᗜ •́ )ᕗ;Flexing;ᕙ( •̀ ᗜ •́ )ᕗ +( ̄ω ̄)/;Yo;( ̄ω ̄)/ +(ಠ_┴);Monocle;(ಠ_┴) +( ͡° 9 ͡°);Smirk;( ͡° 9 ͡°) +(✧ω✧);Dazzling;(✧ω✧) +( •_•)>⌐■-■;Putting Shades On;( •_•)>⌐■-■ +(⌐■_■);Cool Guy;(⌐■_■) +ᕙ(⇀‸↼")ᕗ;Strong;ᕙ(⇀‸↼")ᕗ +( ఠ_ఠ );Inspector;( ఠ_ఠ ) + +[kaomoji_sad] +(T_T);Crying;(T_T) +(ノ﹏ヽ);Sobbing;(ノ﹏ヽ) +(╥﹏╥);Tears;(╥﹏╥) +(._.);Gloomy;(._.) +(u_u);Sorrow;(u_u) +(っω-`。);Wiping Tears;(っω-`。) +(>_<);Pain;(>_<) +( p_q);Depressed;( p_q) +(;(A););Wailing;(;(A);) +(。╯︵╰。);Heartbroken;(。╯︵╰。) +(ಥ﹏ಥ);Sobbing Hard;(ಥ﹏ಥ) +(ノ_<_)^;Down;(ノ_<_)^ +(︶︹︶);Disappointed;(︶︹︶) + +[kaomoji_angry] +(╯°□°)╯︵ ┻━┻;Table Flip;(╯°□°)╯︵ ┻━┻ +(ง'̀-'́)ง;Fight;(ง'̀-'́)ง +(ಠ_ಠ);Disapproval;(ಠ_ಠ) +(¬_¬);Side Eye;(¬_¬) +(╬▔▔);Rage;(╬▔▔) +(ಠ益ಠ);Furious;(ಠ益ಠ) +┬─┬ノ( º _ ºノ);Put Table Back;┬─┬ノ( º _ ºノ) +(ノಠ益ಠ)ノ彡┻━┻;Rage Table Flip;(ノಠ益ಠ)ノ彡┻━┻ +( ಠ ┬─┬ ಠ );Angry Table;( ಠ ┬─┬ ಠ ) +(#`Д´);Mad;(#`Д´) +(≖_≖);Glare;(≖_≖) +(ʘ╬ʘ);Fire Rage;(ʘ╬ʘ) + +[kaomoji_surprised] +(⊙_⊙);Shocked;(⊙_⊙) +(°0°);Gasp;(°0°) +(・_・;);Nervous;(・_・;) +(ⴲ﹏ⴲ);Dizzy;(ⴲ﹏ⴲ) +(O_O);Wide Eyes;(O_O) +(°o°);Astonished;(°o°) +(°△°);Surprised Mouth;(°△°) +(꒪o꒪);Mind Blown;(꒪o꒪) +(⊙_⊙;);Perplexed;(⊙_⊙;) +(º_º);Stunned;(º_º) + +[kaomoji_shrug] +¯\_(ツ)_/¯;Shrug;¯\_(ツ)_/¯ +┐(‘~`;)┌;Whatever;┐(‘~`;)┌ +┐( ̄ヘ ̄)┌;Meh;┐( ̄ヘ ̄)┌ +╮(─▽─)╭;Helpless;╮(─▽─)╭ +¯\_(⊙_ʖ⊙)_/¯;Baffled Shrug;¯\_(⊙_ʖ⊙)_/¯ +¯\_(►_◄)_/¯;Confused Shrug;¯\_(►_◄)_/¯ +┐(‘~` )┌;Unsure;┐(‘~` )┌ +╮(╯_╰)╭;Resigned;╮(╯_╰)╭ + +[kaomoji_animals] +(::^ω^::);Cat;(::^ω^::) +(=^・ω・^=);Kitty;(=^・ω・^=) +(◕ᴥ◕);Bear;(◕ᴥ◕) +ʕ•ᴥ•ʔ;Teddy Bear;ʕ•ᴥ•ʔ +(ᵔᴥᵔ);Puppy;(ᵔᴥᵔ) +(•ᴥ•);Dog;(•ᴥ•) +( ᐢ. ᐢ );Rabbit;( ᐢ. ᐢ ) +( • ɞ • );Bird;( • ɞ • ) +(='o'=);Bunny;(='o'=) +ʕ ᴥ ʔ;Panda;ʕ ᴥ ʔ +(=`ω´=);Fierce Cat;(=`ω´=) diff --git a/app/src/main/java/be/scri/helpers/EmojiAdapter.kt b/app/src/main/java/be/scri/helpers/EmojiAdapter.kt index 5d44583b..54f67d2b 100644 --- a/app/src/main/java/be/scri/helpers/EmojiAdapter.kt +++ b/app/src/main/java/be/scri/helpers/EmojiAdapter.kt @@ -10,17 +10,20 @@ import androidx.recyclerview.widget.RecyclerView import be.scri.R /** - * Displaying emojis and category headers in the emoji palette. + * Displaying emojis, kaomojis, and category headers in the emoji palette. * * @param context The application context. - * @param items The list of items to display, either categories or emojis. + * @param items The list of items to display, either categories, emojis, or kaomojis. + * @param categoryHeaders Localized headers map for categories. * @param itemClick Callback invoked when the user taps an emoji. + * @param kaomojiClick Callback invoked when the user taps a kaomoji. */ class EmojiAdapter( val context: Context, var items: List, val categoryHeaders: Map = emptyMap(), val itemClick: (emoji: EmojiData) -> Unit, + val kaomojiClick: ((kaomoji: KaomojiData) -> Unit)? = null, ) : RecyclerView.Adapter() { private val layoutInflater = LayoutInflater.from(context) @@ -33,6 +36,10 @@ class EmojiAdapter( EmojiViewHolder( layoutInflater.inflate(R.layout.item_emoji, parent, false), ) + ITEM_TYPE_KAOMOJI -> + KaomojiViewHolder( + layoutInflater.inflate(R.layout.item_kaomoji, parent, false), + ) ITEM_TYPE_CATEGORY -> EmojiCategoryViewHolder( layoutInflater.inflate(R.layout.item_emoji_category_title, parent, false), @@ -46,6 +53,7 @@ class EmojiAdapter( ) { when (holder) { is EmojiViewHolder -> holder.bindView(items[position] as Item.Emoji) + is KaomojiViewHolder -> holder.bindView(items[position] as Item.Kaomoji) is EmojiCategoryViewHolder -> holder.bindView(items[position] as Item.Category) } } @@ -53,6 +61,7 @@ class EmojiAdapter( override fun getItemViewType(position: Int): Int = when (items[position]) { is Item.Emoji -> ITEM_TYPE_EMOJI + is Item.Kaomoji -> ITEM_TYPE_KAOMOJI is Item.Category -> ITEM_TYPE_CATEGORY } @@ -72,7 +81,7 @@ class EmojiAdapter( /** * ViewHolder for a single emoji item. * - * @param view The enlarged item_emoji view. + * @param view The item_emoji view. */ inner class EmojiViewHolder( view: android.view.View, @@ -87,10 +96,28 @@ class EmojiAdapter( } } + /** + * ViewHolder for a single kaomoji item. + * + * @param view The item_kaomoji view. + */ + inner class KaomojiViewHolder( + view: android.view.View, + ) : RecyclerView.ViewHolder(view) { + private val kaomojiValue: TextView = view.findViewById(R.id.kaomoji_value) + + fun bindView(kaomoji: Item.Kaomoji) { + kaomojiValue.text = kaomoji.kaomojiData.kaomoji + itemView.setOnClickListener { + kaomojiClick?.invoke(kaomoji.kaomojiData) + } + } + } + /** * ViewHolder for a category header. * - * @param view The enlarged item_emoji_category_title view. + * @param view The item_emoji_category_title view. */ inner class EmojiCategoryViewHolder( view: android.view.View, @@ -111,6 +138,11 @@ class EmojiAdapter( val emojiData: EmojiData, ) : Item + // A single tappable kaomoji. + data class Kaomoji( + val kaomojiData: KaomojiData, + ) : Item + // A category header row. data class Category( val value: String, @@ -119,6 +151,7 @@ class EmojiAdapter( companion object { private const val ITEM_TYPE_EMOJI = 0 - private const val ITEM_TYPE_CATEGORY = 1 + private const val ITEM_TYPE_KAOMOJI = 1 + private const val ITEM_TYPE_CATEGORY = 2 } } diff --git a/app/src/main/java/be/scri/helpers/EmojiHelper.kt b/app/src/main/java/be/scri/helpers/EmojiHelper.kt index f2e035ba..f22d00ae 100644 --- a/app/src/main/java/be/scri/helpers/EmojiHelper.kt +++ b/app/src/main/java/be/scri/helpers/EmojiHelper.kt @@ -84,15 +84,16 @@ data class EmojiData( * @return The drawable resource ID for the category icon. */ fun getCategoryIconRes(category: String): Int = - when (category) { - "smileys_emotion" -> R.drawable.ic_emoji_smileys - "people_body" -> R.drawable.ic_emoji_people - "animals_nature" -> R.drawable.ic_emoji_animals - "food_drink" -> R.drawable.ic_emoji_food - "travel_places" -> R.drawable.ic_emoji_travel - "activities" -> R.drawable.ic_emoji_activities - "objects" -> R.drawable.ic_emoji_objects - "symbols" -> R.drawable.ic_emoji_symbols - "flags" -> R.drawable.ic_emoji_flags + when { + category.startsWith("kaomoji") -> R.drawable.ic_emoji_kaomoji + category == "smileys_emotion" -> R.drawable.ic_emoji_smileys + category == "people_body" -> R.drawable.ic_emoji_people + category == "animals_nature" -> R.drawable.ic_emoji_animals + category == "food_drink" -> R.drawable.ic_emoji_food + category == "travel_places" -> R.drawable.ic_emoji_travel + category == "activities" -> R.drawable.ic_emoji_activities + category == "objects" -> R.drawable.ic_emoji_objects + category == "symbols" -> R.drawable.ic_emoji_symbols + category == "flags" -> R.drawable.ic_emoji_flags else -> R.drawable.ic_emoji_vector } diff --git a/app/src/main/java/be/scri/helpers/KaomojiHelper.kt b/app/src/main/java/be/scri/helpers/KaomojiHelper.kt new file mode 100644 index 00000000..dbbcfeba --- /dev/null +++ b/app/src/main/java/be/scri/helpers/KaomojiHelper.kt @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package be.scri.helpers + +import android.content.Context +import android.util.Log +import java.io.IOException + +const val KAOMOJI_SPEC_FILE_PATH = "kaomoji_spec.txt" +private const val PREF_RECENT_KAOMOJI_KEY = "recent_kaomojis_list" +private const val MAX_RECENT_KAOMOJI_COUNT = 20 + +/** + * Data class representing a single Kaomoji emoticon with its category and display text. + * + * @param category The category this kaomoji belongs to (e.g. kaomoji_joy, kaomoji_sad). + * @param kaomoji The actual kaomoji text string (e.g. ¯\_(ツ)_/¯). + * @param name Optional human readable label/description. + */ +data class KaomojiData( + val category: String, + val kaomoji: String, + val name: String = "", +) + +/** + * Parses raw kaomoji specification asset file. + * + * @param context The application context used to access assets. + * @param path The path to the kaomoji spec file within assets. + * @return A list of [KaomojiData] objects parsed from the asset file. + */ +fun parseRawKaomojiSpecsFile( + context: Context, + path: String = KAOMOJI_SPEC_FILE_PATH, +): List { + val kaomojis = mutableListOf() + var category: String? = null + + try { + context.assets.open(path).bufferedReader().useLines { lines -> + for (line in lines) { + val trimmed = line.trim() + when { + trimmed.startsWith("#") || trimmed.isEmpty() -> continue + trimmed.startsWith("[") && trimmed.endsWith("]") -> { + category = trimmed.substring(1, trimmed.length - 1) + } + else -> { + val parts = trimmed.split(";") + if (parts.isNotEmpty() && category != null) { + val kaomojiText = parts[0].trim() + val name = if (parts.size > 1) parts[1].trim() else "" + if (kaomojiText.isNotEmpty()) { + kaomojis.add(KaomojiData(category!!, kaomojiText, name)) + } + } + } + } + } + } + } catch (e: IOException) { + Log.e("KaomojiHelper", "Error parsing kaomoji spec file: ${e.message}", e) + } + + return kaomojis +} + +/** + * Saves a Kaomoji to the recent history list in SharedPreferences. + */ +fun saveRecentKaomoji( + context: Context, + kaomoji: KaomojiData, +) { + try { + val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) + val currentRecents = getRecentKaomojis(context).map { it.kaomoji }.toMutableList() + currentRecents.remove(kaomoji.kaomoji) + currentRecents.add(0, kaomoji.kaomoji) + if (currentRecents.size > MAX_RECENT_KAOMOJI_COUNT) { + currentRecents.subList(MAX_RECENT_KAOMOJI_COUNT, currentRecents.size).clear() + } + val joined = currentRecents.joinToString("\n") + prefs.edit().putString(PREF_RECENT_KAOMOJI_KEY, joined).apply() + } catch (e: ClassCastException) { + Log.e("KaomojiHelper", "Error saving recent kaomoji: ${e.message}", e) + } +} + +/** + * Retrieves the list of recently used Kaomojis from SharedPreferences. + */ +fun getRecentKaomojis(context: Context): List { + val result = mutableListOf() + try { + val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) + val joined = prefs.getString(PREF_RECENT_KAOMOJI_KEY, "") ?: "" + if (joined.isNotBlank()) { + val lines = joined.split("\n") + for (line in lines) { + if (line.isNotBlank()) { + result.add(KaomojiData("kaomoji_recent", line.trim())) + } + } + } + } catch (e: ClassCastException) { + Log.e("KaomojiHelper", "Error getting recent kaomojis: ${e.message}", e) + } + return result +} diff --git a/app/src/main/java/be/scri/helpers/KeyboardBase.kt b/app/src/main/java/be/scri/helpers/KeyboardBase.kt index 4c1ee3df..ae79d539 100644 --- a/app/src/main/java/be/scri/helpers/KeyboardBase.kt +++ b/app/src/main/java/be/scri/helpers/KeyboardBase.kt @@ -211,7 +211,7 @@ class KeyboardBase { val resources = Resources.getSystem() val sharedPreferences = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) - val conjugateMode = sharedPreferences.getString("conjugate_mode_type", "2x1") + val conjugateMode = sharedPreferences.getString("conjugate_mode_type", "none") defaultHeight = if (conjugateMode != "none") { when (conjugateMode) { diff --git a/app/src/main/res/drawable/ic_emoji_kaomoji.xml b/app/src/main/res/drawable/ic_emoji_kaomoji.xml new file mode 100644 index 00000000..19fc25ea --- /dev/null +++ b/app/src/main/res/drawable/ic_emoji_kaomoji.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_emoji_vector.xml b/app/src/main/res/drawable/ic_emoji_vector.xml index 40d5664a..6cd8c6d3 100644 --- a/app/src/main/res/drawable/ic_emoji_vector.xml +++ b/app/src/main/res/drawable/ic_emoji_vector.xml @@ -1,24 +1,11 @@ - - - - - - - - - + + + + diff --git a/app/src/main/res/drawable/kaomoji_chip_background.xml b/app/src/main/res/drawable/kaomoji_chip_background.xml new file mode 100644 index 00000000..858e408d --- /dev/null +++ b/app/src/main/res/drawable/kaomoji_chip_background.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/input_method_view.xml b/app/src/main/res/layout/input_method_view.xml index 36d913ad..35bad1b9 100644 --- a/app/src/main/res/layout/input_method_view.xml +++ b/app/src/main/res/layout/input_method_view.xml @@ -57,20 +57,44 @@ GeneralKeyboardIME.kt android:background="?android:attr/selectableItemBackgroundBorderless" android:contentDescription="@string/emojis" android:padding="@dimen/small_margin" - android:src="@drawable/ic_arrow_left_vector" - /> + android:src="@drawable/ic_arrow_left_vector" /> - + android:gravity="center_vertical" + android:orientation="horizontal"> + + + + + @@ -111,17 +135,25 @@ GeneralKeyboardIME.kt android:textStyle="normal" android:textSize="18sp" /> - + android:scrollbars="none" + android:fillViewport="true"> + + + + android:src="@drawable/emoji_backspace" /> diff --git a/app/src/main/res/layout/item_kaomoji.xml b/app/src/main/res/layout/item_kaomoji.xml new file mode 100644 index 00000000..cd4ae6b5 --- /dev/null +++ b/app/src/main/res/layout/item_kaomoji.xml @@ -0,0 +1,15 @@ + + + diff --git a/app/src/test/kotlin/be/scri/helpers/KaomojiHelperTest.kt b/app/src/test/kotlin/be/scri/helpers/KaomojiHelperTest.kt new file mode 100644 index 00000000..c9707fdf --- /dev/null +++ b/app/src/test/kotlin/be/scri/helpers/KaomojiHelperTest.kt @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package be.scri.helpers + +import be.scri.R +import org.junit.Assert.assertEquals +import org.junit.Test + +class KaomojiHelperTest { + @Test + fun kaomojiData_holdsPropertiesCorrectly() { + val kaomoji = KaomojiData("kaomoji_joy", "(•‿•)", "Happy") + assertEquals("kaomoji_joy", kaomoji.category) + assertEquals("(•‿•)", kaomoji.kaomoji) + assertEquals("Happy", kaomoji.name) + } + + @Test + fun getCategoryIconRes_returnsKaomojiIcon_forKaomojiCategory() { + assertEquals(R.drawable.ic_emoji_kaomoji, getCategoryIconRes("kaomoji_joy")) + assertEquals(R.drawable.ic_emoji_kaomoji, getCategoryIconRes("kaomoji_sad")) + assertEquals(R.drawable.ic_emoji_kaomoji, getCategoryIconRes("kaomoji_angry")) + assertEquals(R.drawable.ic_emoji_kaomoji, getCategoryIconRes("kaomoji_shrug")) + } + + @Test + fun getCategoryIconRes_returnsDefaultIcons_forStandardCategories() { + assertEquals(R.drawable.ic_emoji_smileys, getCategoryIconRes("smileys_emotion")) + assertEquals(R.drawable.ic_emoji_animals, getCategoryIconRes("animals_nature")) + assertEquals(R.drawable.ic_emoji_vector, getCategoryIconRes("unknown_category")) + } +}