From 3d3971e232de146b96011a5b5a05ced27fd08b80 Mon Sep 17 00:00:00 2001 From: amirisback Date: Sat, 19 Sep 2026 22:45:49 +0700 Subject: [PATCH 1/8] feat: add deleteWordsBeforeCursor method and implement UI updates for keyboard features --- .../appkeyboard/services/KeyboardIME.kt | 32 +++ .../ui/keyboard/autotext/AutoTextKeyboard.kt | 4 +- .../ui/keyboard/form/FormKeyboard.kt | 19 ++ .../ui/keyboard/movie/MovieKeyboard.kt | 3 +- .../ui/keyboard/news/NewsKeyboard.kt | 12 +- .../templatetext/TemplateTextKeyboard.kt | 143 +++++++++---- .../ui/keyboard/webview/WebiewKeyboard.kt | 69 ++++++- .../res/drawable/bg_feature_action_pill.xml | 6 + .../res/drawable/bg_feature_card_12dp.xml | 9 + .../res/drawable/bg_feature_chip_active.xml | 9 + .../res/drawable/bg_feature_chip_inactive.xml | 9 + .../res/drawable/bg_feature_header_item.xml | 10 + .../drawable/bg_feature_icon_container.xml | 5 + .../res/drawable/bg_feature_rating_badge.xml | 11 + .../drawable/bg_feature_toolbar_button.xml | 14 ++ .../main/res/drawable/ic_feature_insert.xml | 9 + .../main/res/drawable/ic_feature_refresh.xml | 9 + .../main/res/drawable/ic_feature_search.xml | 9 + app/src/main/res/drawable/ic_feature_star.xml | 9 + .../res/layout/item_keyboard_autotext.xml | 126 +++++++++--- .../main/res/layout/item_keyboard_header.xml | 36 +++- .../main/res/layout/item_keyboard_movie.xml | 176 +++++++++++----- .../main/res/layout/item_keyboard_news.xml | 121 +++++++++-- .../item_keyboard_template_category.xml | 33 +++ .../layout/item_keyboard_template_text.xml | 72 +++++++ app/src/main/res/layout/keyboard_autotext.xml | 121 ++++++++--- app/src/main/res/layout/keyboard_form.xml | 189 +++++++++++++----- app/src/main/res/layout/keyboard_grid.xml | 68 +++++-- app/src/main/res/layout/keyboard_ime.xml | 2 +- app/src/main/res/layout/keyboard_list.xml | 67 +++++-- .../res/layout/keyboard_template_text.xml | 101 ++++++++++ app/src/main/res/layout/keyboard_webview.xml | 136 ++++++++++--- app/src/main/res/values-night/color.xml | 16 ++ app/src/main/res/values/color.xml | 16 ++ .../KeyboardFeaturesRedesignTest.kt | 122 +++++++++++ frogo-keyboard/build.gradle.kts | 2 + .../libkeyboard/common/core/BaseKeyboard.kt | 14 +- .../common/core/BaseKeyboardIME.kt | 116 ++++++++--- .../libkeyboard/common/core/IKeyboardIME.kt | 2 + .../ui/emoji/AutoGridLayoutManager.kt | 5 +- .../libkeyboard/ui/emoji/EmojiKeyboard.kt | 11 +- .../ui/emoji/RecyclerScrollCallback.kt | 5 - .../libkeyboard/ui/main/MainKeyboard.kt | 140 ++++++++++--- .../ui/main/OnKeyboardActionListener.kt | 5 + .../src/main/res/drawable/keypad_action.xml | 42 ++-- .../src/main/res/drawable/keypad_default.xml | 9 +- .../src/main/res/drawable/keypad_pop_up.xml | 4 +- .../main/res/layout/item_keyboard_main.xml | 6 +- .../src/main/res/values-night/colors.xml | 2 + frogo-keyboard/src/main/res/values/colors.xml | 5 +- frogo-keyboard/src/main/res/values/dimens.xml | 6 +- .../src/main/res/xml/keys_letters_qwerty.xml | 102 +++++++--- .../libkeyboard/BaseKeyboardIMETest.kt | 107 ++++++++++ .../libkeyboard/KeyboardLayoutLogicTest.kt | 52 +++++ 54 files changed, 2006 insertions(+), 422 deletions(-) create mode 100644 app/src/main/res/drawable/bg_feature_action_pill.xml create mode 100644 app/src/main/res/drawable/bg_feature_card_12dp.xml create mode 100644 app/src/main/res/drawable/bg_feature_chip_active.xml create mode 100644 app/src/main/res/drawable/bg_feature_chip_inactive.xml create mode 100644 app/src/main/res/drawable/bg_feature_header_item.xml create mode 100644 app/src/main/res/drawable/bg_feature_icon_container.xml create mode 100644 app/src/main/res/drawable/bg_feature_rating_badge.xml create mode 100644 app/src/main/res/drawable/bg_feature_toolbar_button.xml create mode 100644 app/src/main/res/drawable/ic_feature_insert.xml create mode 100644 app/src/main/res/drawable/ic_feature_refresh.xml create mode 100644 app/src/main/res/drawable/ic_feature_search.xml create mode 100644 app/src/main/res/drawable/ic_feature_star.xml create mode 100644 app/src/main/res/layout/item_keyboard_template_category.xml create mode 100644 app/src/main/res/layout/item_keyboard_template_text.xml create mode 100644 app/src/main/res/layout/keyboard_template_text.xml create mode 100644 app/src/test/java/com/frogobox/appkeyboard/KeyboardFeaturesRedesignTest.kt delete mode 100644 frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/RecyclerScrollCallback.kt create mode 100644 frogo-keyboard/src/test/java/com/frogobox/libkeyboard/BaseKeyboardIMETest.kt create mode 100644 frogo-keyboard/src/test/java/com/frogobox/libkeyboard/KeyboardLayoutLogicTest.kt diff --git a/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt b/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt index 5276f7a..0b5070c 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt @@ -447,6 +447,38 @@ class KeyboardIME : BaseKeyboardIME() { } } + override fun deleteWordsBeforeCursor(count: Int) { + val ic = getActiveInputConnection() ?: return + if (count <= 0) return + val textBefore = ic.getTextBeforeCursor(120, 0)?.toString() ?: return + if (textBefore.isEmpty()) return + + var remainingWords = count + var deleteLen = 0 + var inWord = false + + for (i in textBefore.length - 1 downTo 0) { + val ch = textBefore[i] + if (ch.isWhitespace() || !ch.isLetterOrDigit()) { + if (inWord) { + remainingWords-- + if (remainingWords <= 0) { + deleteLen++ + break + } + inWord = false + } + } else { + inWord = true + } + deleteLen++ + } + + if (deleteLen > 0) { + ic.deleteSurroundingText(deleteLen, 0) + } + } + override fun initView() { suggestionEngine.loadDictionaryFromAsset(this) setupSuggestionBar() diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/autotext/AutoTextKeyboard.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/autotext/AutoTextKeyboard.kt index 1cd69ef..38fe6ae 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/autotext/AutoTextKeyboard.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/autotext/AutoTextKeyboard.kt @@ -34,12 +34,14 @@ class AutoTextKeyboard( super.initUI() binding.apply { tvToolbarTitle.text = "Auto Text" - toolbarManage.setOnClickListener { + val openManage = { val intent = Intent(context, AutoTextActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK } context.startActivity(intent) } + toolbarManage.setOnClickListener { openManage() } + btnAddAutotext.setOnClickListener { openManage() } } } diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboard.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboard.kt index 3a253e8..744fe76 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboard.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboard.kt @@ -34,8 +34,27 @@ class FormKeyboard( super.initUI() binding.apply { btnSubmit.setOnClickListener { + val title = etText.text?.toString()?.trim() + val details = etText2.text?.toString()?.trim() + val number = etText3.text?.toString()?.trim() + + val sb = StringBuilder() + if (!title.isNullOrBlank()) sb.append("Subject: $title\n") + if (!details.isNullOrBlank()) sb.append("Details: $details\n") + if (!number.isNullOrBlank()) sb.append("Ref/No: $number\n") + + if (sb.isNotEmpty()) { + currentInputConnection?.commitText(sb.toString(), 1) + } + KeyboardNavigationHelper.navigateTo(this@FormKeyboard, destinationKeyboard) } + + btnClear.setOnClickListener { + etText.setText("") + etText2.setText("") + etText3.setText("") + } } } diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/movie/MovieKeyboard.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/movie/MovieKeyboard.kt index 6f3a610..36b430e 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/movie/MovieKeyboard.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/movie/MovieKeyboard.kt @@ -43,7 +43,7 @@ class MovieKeyboard( override fun initUI() { super.initUI() binding.apply { - tvToolbarTitle.text = "Movie Api" + tvToolbarTitle.text = "Trending Movies" } } @@ -131,6 +131,7 @@ class MovieKeyboard( frogoRvGridType6TvTitle.text = data.title frogoRvGridType6TvSubtitle.text = data.release_date frogoRvGridType6TvDesc.text = data.overview + frogoRvGridType6TvRating.text = String.format("%.1f", data.vote_average) Glide.with(context).load("${MovieUrl.BASE_URL_IMAGE_ORIGINAL}${data.poster_path}").into(frogoRvGridType6CivPoster) } } diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/news/NewsKeyboard.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/news/NewsKeyboard.kt index eb016bd..c3b11b6 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/news/NewsKeyboard.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/news/NewsKeyboard.kt @@ -16,6 +16,7 @@ import com.frogobox.coreutil.news.NewsUrl import com.frogobox.coreutil.news.model.Article import com.frogobox.coreutil.news.response.ArticleResponse +import com.bumptech.glide.Glide import com.frogobox.libkeyboard.common.core.BaseKeyboard import com.frogobox.recycler.core.FrogoRecyclerNotifyListener import com.frogobox.recycler.core.IFrogoBindingAdapter @@ -49,7 +50,7 @@ class NewsKeyboard( private fun initView() { binding.apply { - tvToolbarTitle.text = "News Api" + tvToolbarTitle.text = "Top Headlines" } } @@ -144,6 +145,15 @@ class NewsKeyboard( ) { binding.apply { tvItemKeyboardMain.text = data.title + tvItemKeyboardDesc.text = data.description ?: "" + tvItemKeyboardSource.text = if (!data.author.isNullOrBlank()) data.author else "News" + if (!data.urlToImage.isNullOrBlank()) { + Glide.with(context) + .load(data.urlToImage) + .into(ivItemKeyboardNews) + } else { + ivItemKeyboardNews.setImageResource(com.frogobox.appkeyboard.R.drawable.ic_menu_news) + } } } } diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboard.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboard.kt index 64a1957..1ef4a8e 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboard.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboard.kt @@ -5,8 +5,11 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.view.ViewGroup import android.widget.LinearLayout -import com.frogobox.appkeyboard.databinding.ItemKeyboardNewsBinding -import com.frogobox.appkeyboard.databinding.KeyboardAutotextBinding +import androidx.core.content.ContextCompat +import com.frogobox.appkeyboard.R +import com.frogobox.appkeyboard.databinding.ItemKeyboardTemplateCategoryBinding +import com.frogobox.appkeyboard.databinding.ItemKeyboardTemplateTextBinding +import com.frogobox.appkeyboard.databinding.KeyboardTemplateTextBinding import com.frogobox.appkeyboard.model.KeyboardFeatureType import com.frogobox.appkeyboard.model.KeyboardFeatureType.TEMPLATE_TEXT_APP import com.frogobox.appkeyboard.model.KeyboardFeatureType.TEMPLATE_TEXT_GAME @@ -19,95 +22,167 @@ import com.frogobox.recycler.core.FrogoRecyclerNotifyListener import com.frogobox.recycler.core.IFrogoBindingAdapter import com.frogobox.recycler.ext.injectorBinding +data class TemplateCategoryItem( + val type: KeyboardFeatureType, + val icon: String, + val title: String, + val isSelected: Boolean +) + class TemplateTextKeyboard( context: Context, attrs: AttributeSet?, -) : BaseKeyboard(context, attrs) { +) : BaseKeyboard(context, attrs) { + + private var currentType: KeyboardFeatureType = TEMPLATE_TEXT_GAME - private var typePlayStore: KeyboardFeatureType? = null + private val categories = listOf( + Pair(TEMPLATE_TEXT_GAME, Pair("🎮", "Game")), + Pair(TEMPLATE_TEXT_APP, Pair("📱", "App")), + Pair(TEMPLATE_TEXT_SALE, Pair("💰", "Sale")), + Pair(TEMPLATE_TEXT_GREETING, Pair("👋", "Greeting")), + Pair(TEMPLATE_TEXT_LOVE, Pair("❤️", "Love")) + ) - override fun setupViewBinding(inflater: LayoutInflater, parent: LinearLayout): KeyboardAutotextBinding { - return KeyboardAutotextBinding.inflate(LayoutInflater.from(context), this, true) + override fun setupViewBinding(inflater: LayoutInflater, parent: LinearLayout): KeyboardTemplateTextBinding { + return KeyboardTemplateTextBinding.inflate(LayoutInflater.from(context), this, true) } override fun initUI() { super.initUI() - typePlayStore?.let { setupContent(it) } + setupCategories() + setupContent(currentType) } fun setupTemplateTextType(templateTextType: KeyboardFeatureType) { - this.typePlayStore = templateTextType + this.currentType = templateTextType + setupCategories() setupContent(templateTextType) } + private fun setupCategories() { + val categoryData = categories.map { (type, meta) -> + TemplateCategoryItem( + type = type, + icon = meta.first, + title = meta.second, + isSelected = type == currentType + ) + } + + val callback = object : IFrogoBindingAdapter { + override fun areContentsTheSame(oldItem: TemplateCategoryItem, newItem: TemplateCategoryItem): Boolean { + return oldItem == newItem + } + + override fun areItemsTheSame(oldItem: TemplateCategoryItem, newItem: TemplateCategoryItem): Boolean { + return oldItem.type == newItem.type + } + + override fun setViewBinding(parent: ViewGroup): ItemKeyboardTemplateCategoryBinding { + return ItemKeyboardTemplateCategoryBinding.inflate(LayoutInflater.from(context), parent, false) + } + + override fun setupInitComponent( + binding: ItemKeyboardTemplateCategoryBinding, + data: TemplateCategoryItem, + position: Int, + notifyListener: FrogoRecyclerNotifyListener + ) { + binding.apply { + tvCategoryIcon.text = data.icon + tvCategoryName.text = data.title + + if (data.isSelected) { + llCategoryContainer.setBackgroundResource(R.drawable.bg_feature_chip_active) + tvCategoryName.setTextColor(ContextCompat.getColor(context, R.color.color_feature_chip_active_text)) + } else { + llCategoryContainer.setBackgroundResource(R.drawable.bg_feature_chip_inactive) + tvCategoryName.setTextColor(ContextCompat.getColor(context, R.color.color_feature_chip_text)) + } + } + } + + override fun onItemClicked( + binding: ItemKeyboardTemplateCategoryBinding, + data: TemplateCategoryItem, + position: Int, + notifyListener: FrogoRecyclerNotifyListener + ) { + if (currentType != data.type) { + currentType = data.type + setupCategories() + setupContent(data.type) + } + } + } + + binding.rvTemplateCategories.injectorBinding() + .addData(categoryData) + .createLayoutLinearHorizontal(false) + .addCallback(callback) + .build() + } + private fun setupContent(templateTextType: KeyboardFeatureType) { val title: String val list: List when (templateTextType) { TEMPLATE_TEXT_GAME -> { - title = getTitleText(TEMPLATE_TEXT_GAME.name) + title = "Game Templates" list = TemplateTextUtils.getTextGame(context) } TEMPLATE_TEXT_APP -> { - title = getTitleText(TEMPLATE_TEXT_APP.name) + title = "App Templates" list = TemplateTextUtils.getTextApp(context) } TEMPLATE_TEXT_SALE -> { - title = getTitleText(TEMPLATE_TEXT_SALE.name) + title = "Sale / Store Templates" list = TemplateTextUtils.getTextSale(context) } TEMPLATE_TEXT_GREETING -> { - title = getTitleText(TEMPLATE_TEXT_GREETING.name) + title = "Greeting Templates" list = TemplateTextUtils.getTextGreeting(context) } TEMPLATE_TEXT_LOVE -> { - title = getTitleText(TEMPLATE_TEXT_LOVE.name) + title = "Love & Sweet Templates" list = TemplateTextUtils.getTextLove(context) } else -> { - title = "" + title = "Quick Templates" list = listOf() } - } binding.tvToolbarTitle.text = title setupRv(list) } - private fun getTitleText(title: String): String { - return title.replace("TEMPLATE_TEXT_", "") - } - private fun setupRv(data: List) { binding.apply { - val adapterCallback = object : - IFrogoBindingAdapter { + IFrogoBindingAdapter { override fun onItemClicked( - binding: ItemKeyboardNewsBinding, + binding: ItemKeyboardTemplateTextBinding, data: TemplateText, position: Int, notifyListener: FrogoRecyclerNotifyListener, ) { - // Your Clicked - val output = data.text - currentInputConnection?.commitText(output, 1) + currentInputConnection?.commitText(data.text, 1) } override fun onItemLongClicked( - binding: ItemKeyboardNewsBinding, + binding: ItemKeyboardTemplateTextBinding, data: TemplateText, position: Int, notifyListener: FrogoRecyclerNotifyListener, - ) { - } + ) {} override fun areContentsTheSame( oldItem: TemplateText, @@ -123,8 +198,8 @@ class TemplateTextKeyboard( return oldItem.id == newItem.id } - override fun setViewBinding(parent: ViewGroup): ItemKeyboardNewsBinding { - return ItemKeyboardNewsBinding.inflate( + override fun setViewBinding(parent: ViewGroup): ItemKeyboardTemplateTextBinding { + return ItemKeyboardTemplateTextBinding.inflate( LayoutInflater.from(context), parent, false @@ -132,18 +207,16 @@ class TemplateTextKeyboard( } override fun setupInitComponent( - binding: ItemKeyboardNewsBinding, + binding: ItemKeyboardTemplateTextBinding, data: TemplateText, position: Int, notifyListener: FrogoRecyclerNotifyListener, ) { - binding.apply { - tvItemKeyboardMain.text = data.text - } + binding.tvItemTemplateText.text = data.text } } - rvKeyboardMain.injectorBinding() + rvKeyboardMain.injectorBinding() .addData(data) .createLayoutLinearVertical(false) .addCallback(adapterCallback) diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/webview/WebiewKeyboard.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/webview/WebiewKeyboard.kt index 2a51fcc..0c075d3 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/webview/WebiewKeyboard.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/webview/WebiewKeyboard.kt @@ -1,12 +1,19 @@ package com.frogobox.appkeyboard.ui.keyboard.webview +import android.annotation.SuppressLint import android.content.Context +import android.graphics.Bitmap import android.util.AttributeSet import android.view.LayoutInflater +import android.view.inputmethod.EditorInfo +import android.webkit.WebChromeClient +import android.webkit.WebView +import android.webkit.WebViewClient import android.widget.LinearLayout import com.frogobox.appkeyboard.databinding.KeyboardWebviewBinding import com.frogobox.libkeyboard.common.core.BaseKeyboard -import com.frogobox.sdk.ext.loadUrlFrogoExt +import com.frogobox.sdk.ext.gone +import com.frogobox.sdk.ext.visible /** * Created by Faisal Amir on 07/11/22 @@ -27,10 +34,68 @@ class WebiewKeyboard( return KeyboardWebviewBinding.inflate(LayoutInflater.from(context), this, true) } + @SuppressLint("SetJavaScriptEnabled") override fun initUI() { super.initUI() binding.apply { - webview.loadUrlFrogoExt("https://www.google.com") + webview.settings.javaScriptEnabled = true + webview.settings.domStorageEnabled = true + + webview.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + pbWebLoading.visible() + url?.let { etSearchUrl.setText(it) } + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + pbWebLoading.gone() + } + } + + webview.webChromeClient = object : WebChromeClient() { + override fun onProgressChanged(view: WebView?, newProgress: Int) { + super.onProgressChanged(view, newProgress) + pbWebLoading.progress = newProgress + if (newProgress >= 100) { + pbWebLoading.gone() + } else { + pbWebLoading.visible() + } + } + } + + val loadUrlOrSearch = { + val query = etSearchUrl.text?.toString()?.trim() ?: "" + val targetUrl = when { + query.startsWith("http://") || query.startsWith("https://") -> query + query.contains(".") && !query.contains(" ") -> "https://$query" + query.isNotEmpty() -> "https://www.google.com/search?q=" + java.net.URLEncoder.encode(query, "UTF-8") + else -> "https://www.google.com" + } + webview.loadUrl(targetUrl) + } + + etSearchUrl.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == EditorInfo.IME_ACTION_GO) { + loadUrlOrSearch() + true + } else false + } + + btnReload.setOnClickListener { + webview.reload() + } + + btnInsertLink.setOnClickListener { + val currentUrl = webview.url + if (!currentUrl.isNullOrBlank()) { + currentInputConnection?.commitText(currentUrl, 1) + } + } + + webview.loadUrl("https://www.google.com") } } diff --git a/app/src/main/res/drawable/bg_feature_action_pill.xml b/app/src/main/res/drawable/bg_feature_action_pill.xml new file mode 100644 index 0000000..781da28 --- /dev/null +++ b/app/src/main/res/drawable/bg_feature_action_pill.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_feature_card_12dp.xml b/app/src/main/res/drawable/bg_feature_card_12dp.xml new file mode 100644 index 0000000..e81c58b --- /dev/null +++ b/app/src/main/res/drawable/bg_feature_card_12dp.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_feature_chip_active.xml b/app/src/main/res/drawable/bg_feature_chip_active.xml new file mode 100644 index 0000000..20126d7 --- /dev/null +++ b/app/src/main/res/drawable/bg_feature_chip_active.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_feature_chip_inactive.xml b/app/src/main/res/drawable/bg_feature_chip_inactive.xml new file mode 100644 index 0000000..d75f2a0 --- /dev/null +++ b/app/src/main/res/drawable/bg_feature_chip_inactive.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_feature_header_item.xml b/app/src/main/res/drawable/bg_feature_header_item.xml new file mode 100644 index 0000000..17318c0 --- /dev/null +++ b/app/src/main/res/drawable/bg_feature_header_item.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_feature_icon_container.xml b/app/src/main/res/drawable/bg_feature_icon_container.xml new file mode 100644 index 0000000..7e5bca2 --- /dev/null +++ b/app/src/main/res/drawable/bg_feature_icon_container.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/bg_feature_rating_badge.xml b/app/src/main/res/drawable/bg_feature_rating_badge.xml new file mode 100644 index 0000000..e6b08fa --- /dev/null +++ b/app/src/main/res/drawable/bg_feature_rating_badge.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_feature_toolbar_button.xml b/app/src/main/res/drawable/bg_feature_toolbar_button.xml new file mode 100644 index 0000000..5953cb5 --- /dev/null +++ b/app/src/main/res/drawable/bg_feature_toolbar_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_feature_insert.xml b/app/src/main/res/drawable/ic_feature_insert.xml new file mode 100644 index 0000000..54dbdd8 --- /dev/null +++ b/app/src/main/res/drawable/ic_feature_insert.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_feature_refresh.xml b/app/src/main/res/drawable/ic_feature_refresh.xml new file mode 100644 index 0000000..6ea380e --- /dev/null +++ b/app/src/main/res/drawable/ic_feature_refresh.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_feature_search.xml b/app/src/main/res/drawable/ic_feature_search.xml new file mode 100644 index 0000000..f83290b --- /dev/null +++ b/app/src/main/res/drawable/ic_feature_search.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_feature_star.xml b/app/src/main/res/drawable/ic_feature_star.xml new file mode 100644 index 0000000..d70e4d4 --- /dev/null +++ b/app/src/main/res/drawable/ic_feature_star.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/item_keyboard_autotext.xml b/app/src/main/res/layout/item_keyboard_autotext.xml index 025b3f5..12924e5 100644 --- a/app/src/main/res/layout/item_keyboard_autotext.xml +++ b/app/src/main/res/layout/item_keyboard_autotext.xml @@ -1,38 +1,98 @@ - - - - - + + + + + + + + - - \ No newline at end of file + android:layout_marginStart="10dp" + android:layout_marginEnd="8dp" + android:orientation="vertical" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/ll_insert_action" + app:layout_constraintStart_toEndOf="@id/fl_icon_badge" + app:layout_constraintTop_toTopOf="parent"> + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_keyboard_header.xml b/app/src/main/res/layout/item_keyboard_header.xml index 164282d..d824049 100644 --- a/app/src/main/res/layout/item_keyboard_header.xml +++ b/app/src/main/res/layout/item_keyboard_header.xml @@ -1,27 +1,45 @@ + android:paddingHorizontal="4dp" + android:paddingVertical="3dp"> - + + + + + + android:textSize="9.5sp" + android:textStyle="bold" + tools:text="News API" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_keyboard_movie.xml b/app/src/main/res/layout/item_keyboard_movie.xml index 0fe31b9..86bfa30 100644 --- a/app/src/main/res/layout/item_keyboard_movie.xml +++ b/app/src/main/res/layout/item_keyboard_movie.xml @@ -1,55 +1,131 @@ - - - - + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:clickable="true" + android:focusable="true" + android:foreground="?attr/selectableItemBackground" + app:cardBackgroundColor="@color/color_feature_card_bg" + app:cardCornerRadius="12dp" + app:cardElevation="1.5dp" + app:cardPreventCornerOverlap="true"> - - - - - - - - - \ No newline at end of file + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="7dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_keyboard_news.xml b/app/src/main/res/layout/item_keyboard_news.xml index 3da3016..700d44e 100644 --- a/app/src/main/res/layout/item_keyboard_news.xml +++ b/app/src/main/res/layout/item_keyboard_news.xml @@ -1,24 +1,111 @@ - - - + + - - \ No newline at end of file + android:orientation="horizontal" + android:padding="10dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_keyboard_template_category.xml b/app/src/main/res/layout/item_keyboard_template_category.xml new file mode 100644 index 0000000..765a891 --- /dev/null +++ b/app/src/main/res/layout/item_keyboard_template_category.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/app/src/main/res/layout/item_keyboard_template_text.xml b/app/src/main/res/layout/item_keyboard_template_text.xml new file mode 100644 index 0000000..ab3a133 --- /dev/null +++ b/app/src/main/res/layout/item_keyboard_template_text.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/keyboard_autotext.xml b/app/src/main/res/layout/keyboard_autotext.xml index c3b768a..b23de7b 100644 --- a/app/src/main/res/layout/keyboard_autotext.xml +++ b/app/src/main/res/layout/keyboard_autotext.xml @@ -3,11 +3,11 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="match_parent"> + android:layout_height="50dp" + android:paddingHorizontal="10dp"> - + app:layout_constraintTop_toTopOf="parent"> + + + + + + + + + android:orientation="vertical" + android:paddingHorizontal="24dp" + android:paddingVertical="20dp" + android:visibility="gone"> + + + + + + + android:textSize="14.5sp" + android:textStyle="bold" /> + + + diff --git a/app/src/main/res/layout/keyboard_form.xml b/app/src/main/res/layout/keyboard_form.xml index 80edbe0..158d815 100644 --- a/app/src/main/res/layout/keyboard_form.xml +++ b/app/src/main/res/layout/keyboard_form.xml @@ -1,6 +1,7 @@ @@ -16,54 +17,86 @@ + android:layout_height="50dp" + android:paddingHorizontal="10dp"> - + app:layout_constraintTop_toTopOf="parent"> + + + + + + + + + android:layout_height="wrap_content" + android:clipToPadding="false" + android:maxHeight="220dp"> + android:padding="12dp"> + app:boxCornerRadiusBottomEnd="12dp" + app:boxCornerRadiusBottomStart="12dp" + app:boxCornerRadiusTopEnd="12dp" + app:boxCornerRadiusTopStart="12dp" + app:boxStrokeColor="@color/color_feature_accent" + app:hintTextColor="@color/color_feature_accent"> - - + android:paddingHorizontal="14dp" + android:paddingVertical="10dp" + android:textColor="@color/tv_text" + android:textSize="13sp" /> @@ -83,39 +116,93 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> - - - + + + + + + - - + + + + + + + android:layout_marginTop="12dp" + android:orientation="horizontal"> + + + + + + diff --git a/app/src/main/res/layout/keyboard_grid.xml b/app/src/main/res/layout/keyboard_grid.xml index 9225523..438d4f2 100644 --- a/app/src/main/res/layout/keyboard_grid.xml +++ b/app/src/main/res/layout/keyboard_grid.xml @@ -3,11 +3,11 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="match_parent"> + android:layout_height="50dp" + android:paddingHorizontal="10dp"> - + app:layout_constraintTop_toTopOf="parent"> + + + + + + + + @@ -64,6 +89,7 @@ android:id="@+id/progressBar" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:indeterminateTint="@color/color_feature_accent" android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/keyboard_ime.xml b/app/src/main/res/layout/keyboard_ime.xml index 553303b..309430c 100644 --- a/app/src/main/res/layout/keyboard_ime.xml +++ b/app/src/main/res/layout/keyboard_ime.xml @@ -33,7 +33,7 @@ android:id="@+id/keyboard_header" android:layout_width="match_parent" android:layout_height="wrap_content" - android:minHeight="@dimen/frogo_dimen_64dp" + android:minHeight="54dp" tools:itemCount="1" tools:listitem="@layout/item_keyboard_header" /> diff --git a/app/src/main/res/layout/keyboard_list.xml b/app/src/main/res/layout/keyboard_list.xml index 1002db6..a4669d2 100644 --- a/app/src/main/res/layout/keyboard_list.xml +++ b/app/src/main/res/layout/keyboard_list.xml @@ -3,11 +3,11 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="match_parent"> + android:layout_height="50dp" + android:paddingHorizontal="10dp"> - + app:layout_constraintTop_toTopOf="parent"> + + + + + + + + @@ -63,6 +89,7 @@ android:id="@+id/progressBar" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:indeterminateTint="@color/color_feature_accent" android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/keyboard_template_text.xml b/app/src/main/res/layout/keyboard_template_text.xml new file mode 100644 index 0000000..04cc8a1 --- /dev/null +++ b/app/src/main/res/layout/keyboard_template_text.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/keyboard_webview.xml b/app/src/main/res/layout/keyboard_webview.xml index 0a74e0c..6b1d2fa 100644 --- a/app/src/main/res/layout/keyboard_webview.xml +++ b/app/src/main/res/layout/keyboard_webview.xml @@ -17,52 +17,132 @@ + android:layout_height="50dp" + android:paddingHorizontal="10dp"> - + app:layout_constraintTop_toTopOf="parent"> - + - + + + + + - + app:layout_constraintTop_toTopOf="parent"> + + + + + + + + - + + + + + diff --git a/app/src/main/res/values-night/color.xml b/app/src/main/res/values-night/color.xml index d24e8ee..68a9f83 100644 --- a/app/src/main/res/values-night/color.xml +++ b/app/src/main/res/values-night/color.xml @@ -20,4 +20,20 @@ #ECEFF1 #37474F + + #21262D + #30363D + #21262D + #F0F6FC + #21262D + #0D47A1 + #4FC3F7 + #C9D1D9 + #8B949E + #16253B + #4FC3F7 + #FFCA28 + #CC000000 + #4FC3F7 + \ No newline at end of file diff --git a/app/src/main/res/values/color.xml b/app/src/main/res/values/color.xml index 4c9034d..600c72d 100644 --- a/app/src/main/res/values/color.xml +++ b/app/src/main/res/values/color.xml @@ -20,4 +20,20 @@ #37474F #E0E0E0 + + @color/bg_card + #18000000 + #0D000000 + @color/tv_text + #F1F3F4 + #E1F5FE + #0288D1 + #4A5568 + #718096 + #EBF8FF + #0288D1 + #FFB300 + #B3000000 + #0288D1 + \ No newline at end of file diff --git a/app/src/test/java/com/frogobox/appkeyboard/KeyboardFeaturesRedesignTest.kt b/app/src/test/java/com/frogobox/appkeyboard/KeyboardFeaturesRedesignTest.kt new file mode 100644 index 0000000..0c9a1e6 --- /dev/null +++ b/app/src/test/java/com/frogobox/appkeyboard/KeyboardFeaturesRedesignTest.kt @@ -0,0 +1,122 @@ +package com.frogobox.appkeyboard + +import com.frogobox.appkeyboard.model.KeyboardFeatureType +import com.frogobox.appkeyboard.ui.keyboard.templatetext.TemplateCategoryItem +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import java.net.URLEncoder + +class KeyboardFeaturesRedesignTest { + + @Test + fun testTemplateCategoriesCoverage() { + val categories = listOf( + TemplateCategoryItem(KeyboardFeatureType.TEMPLATE_TEXT_GAME, "🎮", "Game", true), + TemplateCategoryItem(KeyboardFeatureType.TEMPLATE_TEXT_APP, "📱", "App", false), + TemplateCategoryItem(KeyboardFeatureType.TEMPLATE_TEXT_SALE, "💰", "Sale", false), + TemplateCategoryItem(KeyboardFeatureType.TEMPLATE_TEXT_GREETING, "👋", "Greeting", false), + TemplateCategoryItem(KeyboardFeatureType.TEMPLATE_TEXT_LOVE, "❤️", "Love", false) + ) + + assertEquals(5, categories.size) + assertTrue(categories.first().isSelected) + assertEquals("🎮", categories.first().icon) + assertEquals("Game", categories.first().title) + + // Test category switching simulation + val switched = categories.map { + it.copy(isSelected = it.type == KeyboardFeatureType.TEMPLATE_TEXT_SALE) + } + val selected = switched.first { it.isSelected } + assertEquals(KeyboardFeatureType.TEMPLATE_TEXT_SALE, selected.type) + assertEquals("Sale", selected.title) + } + + @Test + fun testFormKeyboardSnippetBuilder() { + val title = "Invoice #1024" + val details = "2x Mechanical Keyboard Switch Set" + val number = "IDR 350.000" + + val sb = StringBuilder() + if (title.isNotBlank()) sb.append("Subject: $title\n") + if (details.isNotBlank()) sb.append("Details: $details\n") + if (number.isNotBlank()) sb.append("Ref/No: $number\n") + + val expected = "Subject: Invoice #1024\nDetails: 2x Mechanical Keyboard Switch Set\nRef/No: IDR 350.000\n" + assertEquals(expected, sb.toString()) + } + + @Test + fun testFormKeyboardSnippetBuilderWithPartialFields() { + val title = "" + val details = "Quick reminder for the meeting at 3 PM" + val number = "" + + val sb = StringBuilder() + if (title.isNotBlank()) sb.append("Subject: $title\n") + if (details.isNotBlank()) sb.append("Details: $details\n") + if (number.isNotBlank()) sb.append("Ref/No: $number\n") + + val expected = "Details: Quick reminder for the meeting at 3 PM\n" + assertEquals(expected, sb.toString()) + } + + @Test + fun testMovieRatingFormatting() { + val vote1 = 8.423 + val vote2 = 7.0 + val vote3 = 9.99 + + assertEquals("8.4", String.format(java.util.Locale.US, "%.1f", vote1)) + assertEquals("7.0", String.format(java.util.Locale.US, "%.1f", vote2)) + assertEquals("10.0", String.format(java.util.Locale.US, "%.1f", vote3)) + } + + @Test + fun testNewsSourceFallbackLogic() { + val authorValid: String? = "BBC News" + val authorBlank: String? = " " + val authorNull: String? = null + + val source1 = if (!authorValid.isNullOrBlank()) authorValid else "News" + val source2 = if (!authorBlank.isNullOrBlank()) authorBlank else "News" + val source3 = if (!authorNull.isNullOrBlank()) authorNull else "News" + + assertEquals("BBC News", source1) + assertEquals("News", source2) + assertEquals("News", source3) + } + + @Test + fun testWebviewUrlSearchResolution() { + fun resolveUrl(query: String): String { + val trimmed = query.trim() + return when { + trimmed.startsWith("http://") || trimmed.startsWith("https://") -> trimmed + trimmed.contains(".") && !trimmed.contains(" ") -> "https://$trimmed" + trimmed.isNotEmpty() -> "https://www.google.com/search?q=" + URLEncoder.encode(trimmed, "UTF-8") + else -> "https://www.google.com" + } + } + + assertEquals("https://github.com", resolveUrl("https://github.com")) + assertEquals("http://example.com", resolveUrl("http://example.com")) + assertEquals("https://kotlinlang.org", resolveUrl("kotlinlang.org")) + assertEquals("https://www.google.com/search?q=android+keyboard+design", resolveUrl("android keyboard design")) + assertEquals("https://www.google.com", resolveUrl("")) + } + + @Test + fun testFeatureTypeFromIdMapping() { + assertEquals(KeyboardFeatureType.AUTO_TEXT, KeyboardFeatureType.from("menu_auto_text")) + assertEquals(KeyboardFeatureType.MOVIE, KeyboardFeatureType.from("menu_movie")) + assertEquals(KeyboardFeatureType.NEWS, KeyboardFeatureType.from("menu_news")) + assertEquals(KeyboardFeatureType.WEB, KeyboardFeatureType.from("menu_web")) + assertEquals(KeyboardFeatureType.FORM, KeyboardFeatureType.from("menu_form")) + assertEquals(KeyboardFeatureType.TEMPLATE_TEXT_GAME, KeyboardFeatureType.from("menu_play_store_game")) + assertEquals(KeyboardFeatureType.AUTO_TEXT, KeyboardFeatureType.from("unknown_invalid_id")) + } +} diff --git a/frogo-keyboard/build.gradle.kts b/frogo-keyboard/build.gradle.kts index c8439dd..49cab6e 100644 --- a/frogo-keyboard/build.gradle.kts +++ b/frogo-keyboard/build.gradle.kts @@ -61,6 +61,8 @@ dependencies { api(libs.frogo.consume.api) api(libs.androidx.emoji2) + testImplementation(libs.junit) + } afterEvaluate { diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboard.kt b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboard.kt index 0074930..026622d 100644 --- a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboard.kt +++ b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboard.kt @@ -36,7 +36,12 @@ abstract class BaseKeyboard @JvmOverloads constructor( // Backing property for view binding private var _binding: VB? = null val binding: VB - get() = _binding ?: throw IllegalStateException("Binding is accessed before setup or after onDestroy()") + get() { + if (_binding == null) { + _binding = setupViewBinding(LayoutInflater.from(context), this) + } + return _binding!! + } // Current input connection to communicate with the EditText / InputMethod var currentInputConnection: InputConnection? = null @@ -72,8 +77,10 @@ abstract class BaseKeyboard @JvmOverloads constructor( } private fun initialize() { - _binding = setupViewBinding(LayoutInflater.from(context), this) - onCreate() + if (_binding == null) { + _binding = setupViewBinding(LayoutInflater.from(context), this) + onCreate() + } } /** @@ -87,7 +94,6 @@ abstract class BaseKeyboard @JvmOverloads constructor( override fun onDetachedFromWindow() { super.onDetachedFromWindow() onDestroy() - _binding = null currentInputConnection = null } } diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboardIME.kt b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboardIME.kt index 16aea89..0076ae8 100644 --- a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboardIME.kt +++ b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboardIME.kt @@ -30,6 +30,7 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo companion object { // How quickly do we have to doubletap shift to enable permanent caps lock const val SHIFT_PERM_TOGGLE_SPEED = 500L + const val DOUBLE_SPACE_PERIOD_TIMEOUT = 350L const val KEYBOARD_LETTERS = 0 const val KEYBOARD_SYMBOLS = 1 @@ -40,6 +41,7 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo var keyboard: ItemMainKeyboard? = null var lastShiftPressTS = 0L + var lastSpacePressTS = 0L var keyboardMode = KEYBOARD_LETTERS var inputTypeClass = InputType.TYPE_CLASS_TEXT var enterKeyType = IME_ACTION_NONE @@ -211,6 +213,7 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo when (code) { ItemMainKeyboard.KEYCODE_DELETE -> { + lastSpacePressTS = 0L if (kb.mShiftState == SHIFT_ON_ONE_CHAR) { kb.mShiftState = SHIFT_OFF } @@ -227,13 +230,10 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo inputConnection.commitText("", 1) } - if (inputConnection != currentInputConnection) { - inputConnection.deleteSurroundingText(1, 0) - } - invalidateAllKeys() } ItemMainKeyboard.KEYCODE_SHIFT -> { + lastSpacePressTS = 0L if (keyboardMode == KEYBOARD_LETTERS) { when { kb.mShiftState == SHIFT_ON_PERMANENT -> kb.mShiftState = SHIFT_OFF @@ -257,6 +257,7 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo invalidateAllKeys() } ItemMainKeyboard.KEYCODE_ENTER -> { + lastSpacePressTS = 0L val imeOptionsActionId = getImeOptionsActionId() if (imeOptionsActionId != IME_ACTION_NONE) { inputConnection.performEditorAction(imeOptionsActionId) @@ -268,12 +269,9 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER) ) } - - if (inputConnection != currentInputConnection) { - inputConnection.commitText("\n", 1) - } } ItemMainKeyboard.KEYCODE_MODE_CHANGE -> { + lastSpacePressTS = 0L val keyboardXml = if (keyboardMode == KEYBOARD_LETTERS) { keyboardMode = KEYBOARD_SYMBOLS R.xml.keys_symbols @@ -285,9 +283,11 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo initialSetupKeyboard() } ItemMainKeyboard.KEYCODE_EMOJI -> { + lastSpacePressTS = 0L runEmojiBoard() } ItemMainKeyboard.KEYCODE_TAB -> { + lastSpacePressTS = 0L val isMultiline = (currentInputEditorInfo?.inputType ?: 0) and InputType.TYPE_TEXT_FLAG_MULTI_LINE != 0 if (isMultiline) { inputConnection.commitText("\t", 1) @@ -301,12 +301,15 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo } } ItemMainKeyboard.KEYCODE_ARROW_LEFT -> { + lastSpacePressTS = 0L moveCursor(false) } ItemMainKeyboard.KEYCODE_ARROW_RIGHT -> { + lastSpacePressTS = 0L moveCursor(true) } ItemMainKeyboard.KEYCODE_ARROW_UP -> { + lastSpacePressTS = 0L inputConnection.sendKeyEvent( KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_UP) ) @@ -315,6 +318,7 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo ) } ItemMainKeyboard.KEYCODE_ARROW_DOWN -> { + lastSpacePressTS = 0L inputConnection.sendKeyEvent( KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN) ) @@ -329,16 +333,35 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo codeChar = Character.toUpperCase(codeChar) } - // If the keyboard is set to symbols and the user presses space, we usually should switch back to the letters keyboard. - // However, avoid doing that in cases when the EditText for example requires numbers as the input. - // We can detect that by the text not changing on pressing Space. - if (keyboardMode != KEYBOARD_LETTERS && code == ItemMainKeyboard.KEYCODE_SPACE) { - val originalText = - inputConnection.getExtractedText(ExtractedTextRequest(), 0)?.text ?: return - inputConnection.commitText(codeChar.toString(), 1) - val newText = inputConnection.getExtractedText(ExtractedTextRequest(), 0)?.text - switchToLetters = newText != null && originalText != newText + if (code == ItemMainKeyboard.KEYCODE_SPACE) { + val now = System.currentTimeMillis() + // Gboard behavior: Double tap space -> insert ". " and auto-shift + if (keyboardMode == KEYBOARD_LETTERS && (now - lastSpacePressTS) < DOUBLE_SPACE_PERIOD_TIMEOUT) { + val textBefore = inputConnection.getTextBeforeCursor(2, 0) + if (textBefore != null && textBefore.length >= 1 && textBefore.endsWith(" ") && !textBefore.endsWith(". ")) { + inputConnection.deleteSurroundingText(1, 0) + inputConnection.commitText(". ", 1) + lastSpacePressTS = 0L + if (kb.mShiftState == SHIFT_OFF) { + kb.mShiftState = SHIFT_ON_ONE_CHAR + invalidateAllKeys() + } + updateShiftKeyState() + return + } + } + lastSpacePressTS = now + + inputConnection.commitText(" ", 1) + + if (keyboardMode != KEYBOARD_LETTERS && + inputTypeClass != TYPE_CLASS_NUMBER && + inputTypeClass != TYPE_CLASS_PHONE && + inputTypeClass != TYPE_CLASS_DATETIME) { + switchToLetters = true + } } else { + lastSpacePressTS = 0L inputConnection.commitText(codeChar.toString(), 1) } @@ -355,24 +378,63 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo } override fun moveCursor(moveRight: Boolean) { - val extractedText = - currentInputConnection?.getExtractedText(ExtractedTextRequest(), 0) ?: return - var newCursorPosition = extractedText.selectionStart - val textLength = extractedText.text?.length ?: return - newCursorPosition = if (moveRight) { - (newCursorPosition + 1).coerceAtMost(textLength) + val ic = currentInputConnection ?: return + val extractedText = ic.getExtractedText(ExtractedTextRequest(), 0) + if (extractedText != null && extractedText.text != null) { + var newCursorPosition = extractedText.selectionStart + val textLength = extractedText.text.length + newCursorPosition = if (moveRight) { + (newCursorPosition + 1).coerceAtMost(textLength) + } else { + (newCursorPosition - 1).coerceAtLeast(0) + } + ic.setSelection(newCursorPosition, newCursorPosition) } else { - (newCursorPosition - 1).coerceAtLeast(0) + // Fallback for custom editors or WebViews that don't support ExtractedText + val keyCode = if (moveRight) KeyEvent.KEYCODE_DPAD_RIGHT else KeyEvent.KEYCODE_DPAD_LEFT + ic.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode)) + ic.sendKeyEvent(KeyEvent(KeyEvent.ACTION_UP, keyCode)) + } + } + + override fun deleteWordsBeforeCursor(count: Int) { + val ic = currentInputConnection ?: return + if (count <= 0) return + val textBefore = ic.getTextBeforeCursor(120, 0)?.toString() ?: return + if (textBefore.isEmpty()) return + + var remainingWords = count + var deleteLen = 0 + var inWord = false + + for (i in textBefore.length - 1 downTo 0) { + val ch = textBefore[i] + if (ch.isWhitespace() || !ch.isLetterOrDigit()) { + if (inWord) { + remainingWords-- + if (remainingWords <= 0) { + deleteLen++ + break + } + inWord = false + } + } else { + inWord = true + } + deleteLen++ } - currentInputConnection?.setSelection(newCursorPosition, newCursorPosition) + if (deleteLen > 0) { + ic.deleteSurroundingText(deleteLen, 0) + } } override fun getImeOptionsActionId(): Int { - return if (currentInputEditorInfo.imeOptions and IME_FLAG_NO_ENTER_ACTION != 0) { + val editorInfo = currentInputEditorInfo ?: return IME_ACTION_NONE + return if (editorInfo.imeOptions and IME_FLAG_NO_ENTER_ACTION != 0) { IME_ACTION_NONE } else { - currentInputEditorInfo.imeOptions and IME_MASK_ACTION + editorInfo.imeOptions and IME_MASK_ACTION } } diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/IKeyboardIME.kt b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/IKeyboardIME.kt index 46f1951..67baac1 100644 --- a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/IKeyboardIME.kt +++ b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/IKeyboardIME.kt @@ -54,6 +54,8 @@ interface IKeyboardIME { fun moveCursor(moveRight: Boolean) + fun deleteWordsBeforeCursor(count: Int) + fun getImeOptionsActionId(): Int fun getKeyboardLayoutXML(): Int diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/AutoGridLayoutManager.kt b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/AutoGridLayoutManager.kt index 0cfbbe9..19d3281 100644 --- a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/AutoGridLayoutManager.kt +++ b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/AutoGridLayoutManager.kt @@ -31,7 +31,10 @@ class AutoGridLayoutManager( } else { height - paddingTop - paddingBottom } - spanCount = max(1, totalSpace / itemWidth) + val calculatedSpan = max(1, totalSpace / itemWidth) + if (spanCount != calculatedSpan) { + spanCount = calculatedSpan + } } super.onLayoutChildren(recycler, state) } diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboard.kt b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboard.kt index ed19597..354436a 100644 --- a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboard.kt +++ b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboard.kt @@ -203,11 +203,16 @@ class EmojiKeyboard( } } + private val backgroundExecutor = java.util.concurrent.Executors.newSingleThreadExecutor() + + override fun onDestroy() { + super.onDestroy() + backgroundExecutor.shutdown() + } + private fun ensureBackgroundThread(callback: () -> Unit) { if (Looper.myLooper() == Looper.getMainLooper()) { - Thread { - callback() - }.start() + backgroundExecutor.execute(callback) } else { callback() } diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/RecyclerScrollCallback.kt b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/RecyclerScrollCallback.kt deleted file mode 100644 index fba5fec..0000000 --- a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/RecyclerScrollCallback.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.frogobox.libkeyboard.ui.emoji - -interface RecyclerScrollCallback { - fun onScrolled(scrollY: Int) -} \ No newline at end of file diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt index f265cfc..496d857 100644 --- a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt +++ b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt @@ -124,8 +124,22 @@ class MainKeyboard @JvmOverloads constructor( private var mTopSmallNumberMarginWidth = 0f private var mTopSmallNumberMarginHeight = 0f private val mSpaceMoveThreshold: Int + private var mDeleteSwipeThreshold = 0 private var ignoreTouches = false + // Instant Gboard gestures + private var mSpaceStartX = 0 + private var mIsSlidingSpace = false + private var mDeleteStartX = 0 + private var mIsSlidingDelete = false + + // Cached drawables for zero per-frame allocation + private var mKeypadDefaultDrawable: android.graphics.drawable.Drawable? = null + private var mKeypadActionDrawable: android.graphics.drawable.Drawable? = null + private var mShiftOffDrawable: android.graphics.drawable.Drawable? = null + private var mShiftOneCharDrawable: android.graphics.drawable.Drawable? = null + private var mShiftPermDrawable: android.graphics.drawable.Drawable? = null + /** Reusable Paint for top small numbers — avoids per-frame allocation */ private val mSmallLetterPaint = Paint() @@ -209,6 +223,7 @@ class MainKeyboard @JvmOverloads constructor( mLabelTextSize = resources.getDimension(R.dimen.label_text_size).toInt() mPreviewHeight = resources.getDimension(R.dimen.key_height).toInt() mSpaceMoveThreshold = resources.getDimension(com.frogobox.ui.R.dimen.frogo_dimen_8dp).toInt() + mDeleteSwipeThreshold = (resources.getDimension(R.dimen.key_height) * 0.6f).toInt() mPreviewText = inflater.inflate(resources.getLayout(R.layout.item_keyboard_main), null) as TextView mPreviewTextSizeLarge = resources.getDimension(R.dimen.preview_text_size).toInt() @@ -237,6 +252,16 @@ class MainKeyboard @JvmOverloads constructor( mTopSmallNumberSize = resources.getDimension(com.frogobox.ui.R.dimen.frogo_dimen_font_10sp) mTopSmallNumberMarginWidth = resources.getDimension(R.dimen.top_small_number_margin_width) mTopSmallNumberMarginHeight = resources.getDimension(R.dimen.top_small_number_margin_height) + + initCachedDrawables() + } + + fun initCachedDrawables() { + mKeypadDefaultDrawable = resources.getDrawable(R.drawable.keypad_default, context.theme) + mKeypadActionDrawable = resources.getDrawable(R.drawable.keypad_action, context.theme) + mShiftOffDrawable = ResourcesCompat.getDrawable(resources, R.drawable.ic_keyboard_caps_outline, null) + mShiftOneCharDrawable = ResourcesCompat.getDrawable(resources, R.drawable.ic_keyboard_caps, null) + mShiftPermDrawable = ResourcesCompat.getDrawable(resources, R.drawable.ic_keyboard_caps_underlined, null) } override fun onAttachedToWindow() { @@ -265,6 +290,7 @@ class MainKeyboard @JvmOverloads constructor( if (visibility == VISIBLE) { mTextColor = context.getColorExt(R.color.keypad_text) mBackgroundColor = context.getColorExt(R.color.keyboard_board) + initCachedDrawables() val strokeColor = context.getColorExt(R.color.keypad_mini_stroke) val miniKeyboardBackgroundColor = context.getColorExt(R.color.keypad) @@ -441,12 +467,14 @@ class MainKeyboard @JvmOverloads constructor( canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) val keyCount = keys.size + val kb = mKeyboard + val defaultBg = mKeypadDefaultDrawable ?: resources.getDrawable(R.drawable.keypad_default, context.theme) + val actionBg = mKeypadActionDrawable ?: resources.getDrawable(R.drawable.keypad_action, context.theme) + for (i in 0 until keyCount) { val key = keys[i] val code = key.code - var keyBackground = resources.getDrawable(R.drawable.keypad_default, context.theme) - when (code) { - + val keyBackground = when (code) { KEYCODE_SHIFT, KEYCODE_DELETE, KEYCODE_ENTER, @@ -455,15 +483,13 @@ class MainKeyboard @JvmOverloads constructor( KEYCODE_ARROW_LEFT, KEYCODE_ARROW_RIGHT, KEYCODE_ARROW_UP, - KEYCODE_ARROW_DOWN -> { - keyBackground = resources.getDrawable(R.drawable.keypad_action, context.theme) - } - + KEYCODE_ARROW_DOWN -> actionBg + else -> defaultBg } // Switch the character to uppercase if shift is pressed val label = adjustCase(key.label)?.toString() - val bounds = keyBackground!!.bounds + val bounds = keyBackground.bounds if (key.width != bounds.right || key.height != bounds.bottom) { keyBackground.setBounds(0, 0, key.width, key.height) } @@ -506,15 +532,13 @@ class MainKeyboard @JvmOverloads constructor( // Turn off drop shadow paint.setShadowLayer(0f, 0f, 0f, 0) - } else if (key.icon != null && mKeyboard != null) { - val kb = mKeyboard!! + } else if (key.icon != null && kb != null) { if (code == KEYCODE_SHIFT) { - val drawableId = when (kb.mShiftState) { - SHIFT_OFF -> R.drawable.ic_keyboard_caps_outline - SHIFT_ON_ONE_CHAR -> R.drawable.ic_keyboard_caps - else -> R.drawable.ic_keyboard_caps_underlined + key.icon = when (kb.mShiftState) { + SHIFT_OFF -> mShiftOffDrawable + SHIFT_ON_ONE_CHAR -> mShiftOneCharDrawable + else -> mShiftPermDrawable } - key.icon = ResourcesCompat.getDrawable(resources, drawableId, null) } key.icon?.let { icon -> @@ -1037,6 +1061,19 @@ class MainKeyboard @JvmOverloads constructor( mDownTime = me.eventTime mLastMoveTime = mDownTime + val isSpaceKey = mCurrentKey >= 0 && mKeys.getOrNull(mCurrentKey)?.code == KEYCODE_SPACE + val isDeleteKey = mCurrentKey >= 0 && mKeys.getOrNull(mCurrentKey)?.code == KEYCODE_DELETE + + if (isSpaceKey) { + mSpaceStartX = touchX + mLastSpaceMoveX = touchX + mIsSlidingSpace = false + } + if (isDeleteKey) { + mDeleteStartX = touchX + mIsSlidingDelete = false + } + val onPressKey = if (keyIndex != NOT_A_KEY) { mKeys[keyIndex].code } else { @@ -1051,9 +1088,8 @@ class MainKeyboard @JvmOverloads constructor( val msg = mHandler!!.obtainMessage(MSG_REPEAT) mHandler!!.sendMessageDelayed(msg, REPEAT_START_DELAY.toLong()) - // if the user long presses Space, move the cursor after swipine left/right if (mKeys[mCurrentKey].code == KEYCODE_SPACE) { - mLastSpaceMoveX = -1 + mLastSpaceMoveX = touchX } else { repeatKey(true) } @@ -1096,7 +1132,45 @@ class MainKeyboard @JvmOverloads constructor( } } - if (mIsLongPressingSpace) { + // Instant Gboard spacebar swipe cursor control (without 400ms delay) + if (mCurrentKey >= 0 && mKeys.getOrNull(mCurrentKey)?.code == KEYCODE_SPACE) { + val totalDiffX = touchX - mSpaceStartX + if (kotlin.math.abs(totalDiffX) > mSpaceMoveThreshold) { + if (!mIsSlidingSpace) { + mIsSlidingSpace = true + mHandler?.removeMessages(MSG_REPEAT) + mHandler?.removeMessages(MSG_LONGPRESS) + showPreview(NOT_A_KEY) + } + + val diff = touchX - mLastSpaceMoveX + if (diff < -mSpaceMoveThreshold) { + val steps = (-diff) / mSpaceMoveThreshold + for (i in 0 until steps) { + mOnKeyboardActionListener?.moveCursorLeft() + vibrateIfNeeded() + } + mLastSpaceMoveX -= steps * mSpaceMoveThreshold + } else if (diff > mSpaceMoveThreshold) { + val steps = diff / mSpaceMoveThreshold + for (i in 0 until steps) { + mOnKeyboardActionListener?.moveCursorRight() + vibrateIfNeeded() + } + mLastSpaceMoveX += steps * mSpaceMoveThreshold + } + } + } else if (mCurrentKey >= 0 && mKeys.getOrNull(mCurrentKey)?.code == KEYCODE_DELETE) { + val swipeLeftDist = mDeleteStartX - touchX + if (swipeLeftDist > mDeleteSwipeThreshold) { + if (!mIsSlidingDelete) { + mIsSlidingDelete = true + mHandler?.removeMessages(MSG_REPEAT) + mHandler?.removeMessages(MSG_LONGPRESS) + showPreview(NOT_A_KEY) + } + } + } else if (mIsLongPressingSpace) { if (mLastSpaceMoveX == -1) { mLastSpaceMoveX = mLastX } @@ -1148,12 +1222,28 @@ class MainKeyboard @JvmOverloads constructor( } showPreview(NOT_A_KEY) Arrays.fill(mKeyIndices, NOT_A_KEY) - // If we're not on a repeating key (which sends on a DOWN event) - if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) { - detectAndSendKey(mCurrentKey, touchX, touchY, eventTime) - } else if (mRepeatKeyIndex != NOT_A_KEY && mKeys.getOrNull(mCurrentKey)?.code == KEYCODE_SPACE && !mIsLongPressingSpace) { - // Space key was in repeat mode but user released before long-press activated - detectAndSendKey(mCurrentKey, touchX, touchY, eventTime) + + val wasSlidingSpace = mIsSlidingSpace + val wasSlidingDelete = mIsSlidingDelete + mIsSlidingSpace = false + mIsSlidingDelete = false + + if (wasSlidingSpace) { + // Sliding space gesture consumed - do not send space character + } else if (wasSlidingDelete) { + // Sliding delete gesture consumed - delete words + val swipeLeftDist = mDeleteStartX - touchX + val wordsToDelete = max(1, swipeLeftDist / (mDeleteSwipeThreshold * 2)) + mOnKeyboardActionListener?.deleteWordsBeforeCursor(wordsToDelete) + vibrateIfNeeded() + } else { + // If we're not on a repeating key (which sends on a DOWN event) + if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) { + detectAndSendKey(mCurrentKey, touchX, touchY, eventTime) + } else if (mRepeatKeyIndex != NOT_A_KEY && mKeys.getOrNull(mCurrentKey)?.code == KEYCODE_SPACE && !mIsLongPressingSpace) { + // Space key was in repeat mode but user released before long-press activated + detectAndSendKey(mCurrentKey, touchX, touchY, eventTime) + } } invalidateKey(keyIndex) @@ -1164,6 +1254,8 @@ class MainKeyboard @JvmOverloads constructor( MotionEvent.ACTION_CANCEL -> { mIsLongPressingSpace = false + mIsSlidingSpace = false + mIsSlidingDelete = false mLastSpaceMoveX = 0 removeMessages() dismissPopupKeyboard() diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/OnKeyboardActionListener.kt b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/OnKeyboardActionListener.kt index 6bbcb5a..48746d7 100644 --- a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/OnKeyboardActionListener.kt +++ b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/OnKeyboardActionListener.kt @@ -43,4 +43,9 @@ interface OnKeyboardActionListener { * @param text the string to be displayed. */ fun onText(text: String) + + /** + * Called when the user swipes left on Backspace to delete words + */ + fun deleteWordsBeforeCursor(count: Int) {} } \ No newline at end of file diff --git a/frogo-keyboard/src/main/res/drawable/keypad_action.xml b/frogo-keyboard/src/main/res/drawable/keypad_action.xml index 4824432..4b88a25 100644 --- a/frogo-keyboard/src/main/res/drawable/keypad_action.xml +++ b/frogo-keyboard/src/main/res/drawable/keypad_action.xml @@ -1,14 +1,32 @@ - - - - - - + + + + + + + + + + - + + + + + + + + + + + diff --git a/frogo-keyboard/src/main/res/drawable/keypad_default.xml b/frogo-keyboard/src/main/res/drawable/keypad_default.xml index cdc762a..05f520f 100644 --- a/frogo-keyboard/src/main/res/drawable/keypad_default.xml +++ b/frogo-keyboard/src/main/res/drawable/keypad_default.xml @@ -6,8 +6,7 @@ android:bottom="@dimen/key_margin" android:left="@dimen/key_margin" android:right="@dimen/key_margin" - android:top="@dimen/key_margin" - android:state_pressed="true"> + android:top="@dimen/key_margin"> @@ -21,10 +20,10 @@ android:bottom="@dimen/key_margin" android:left="@dimen/key_margin" android:right="@dimen/key_margin" - android:top="@dimen/key_margin" - android:state_pressed="true"> + android:top="@dimen/key_margin"> - diff --git a/frogo-keyboard/src/main/res/drawable/keypad_pop_up.xml b/frogo-keyboard/src/main/res/drawable/keypad_pop_up.xml index aa6c2bc..922f557 100644 --- a/frogo-keyboard/src/main/res/drawable/keypad_pop_up.xml +++ b/frogo-keyboard/src/main/res/drawable/keypad_pop_up.xml @@ -3,7 +3,7 @@ - + @@ -13,7 +13,7 @@ android:width="@dimen/frogo_dimen_1dp" android:color="@color/keypad_mini_stroke" /> - + diff --git a/frogo-keyboard/src/main/res/layout/item_keyboard_main.xml b/frogo-keyboard/src/main/res/layout/item_keyboard_main.xml index bb16d0c..771b582 100644 --- a/frogo-keyboard/src/main/res/layout/item_keyboard_main.xml +++ b/frogo-keyboard/src/main/res/layout/item_keyboard_main.xml @@ -4,5 +4,7 @@ android:background="@drawable/keypad_pop_up" android:gravity="center" android:includeFontPadding="false" - android:textColor="#FFFFFF" - android:textSize="24sp" /> + android:padding="@dimen/key_margin" + android:textColor="@color/keypad_text" + android:textStyle="bold" + android:textSize="@dimen/preview_text_size" /> diff --git a/frogo-keyboard/src/main/res/values-night/colors.xml b/frogo-keyboard/src/main/res/values-night/colors.xml index 45d4fcd..3b30b0c 100644 --- a/frogo-keyboard/src/main/res/values-night/colors.xml +++ b/frogo-keyboard/src/main/res/values-night/colors.xml @@ -13,10 +13,12 @@ #F9FCFF #80232427 + #3F4347 #232427 #80232427 #181921 #80B1BEFF + #808595D6 \ No newline at end of file diff --git a/frogo-keyboard/src/main/res/values/colors.xml b/frogo-keyboard/src/main/res/values/colors.xml index b7d9199..4a677b4 100644 --- a/frogo-keyboard/src/main/res/values/colors.xml +++ b/frogo-keyboard/src/main/res/values/colors.xml @@ -29,12 +29,13 @@ #001E2C #80FFFFFF #80EFEFEF - #757575 + #D1E4FF #FFFFFF #8076D1FF + #8038A4E0 - #757575 + #B0BEC5 #FFFFFF diff --git a/frogo-keyboard/src/main/res/values/dimens.xml b/frogo-keyboard/src/main/res/values/dimens.xml index 4974eff..653d829 100644 --- a/frogo-keyboard/src/main/res/values/dimens.xml +++ b/frogo-keyboard/src/main/res/values/dimens.xml @@ -3,10 +3,10 @@ 52dp 3dp - 8dp + 10dp 16dp - 12dp - 16dp + 10dp + 14dp 60dp -10dp diff --git a/frogo-keyboard/src/main/res/xml/keys_letters_qwerty.xml b/frogo-keyboard/src/main/res/xml/keys_letters_qwerty.xml index 7303258..e2aba2f 100644 --- a/frogo-keyboard/src/main/res/xml/keys_letters_qwerty.xml +++ b/frogo-keyboard/src/main/res/xml/keys_letters_qwerty.xml @@ -14,12 +14,12 @@ app:topSmallNumber="2" /> + app:popupCharacters="@áàâãäåāæą" + app:popupKeyboard="@xml/keys_popup_template" + app:topSmallNumber="@" /> + app:popupCharacters="#śßš$" + app:popupKeyboard="@xml/keys_popup_template" + app:topSmallNumber="#" /> + app:popupCharacters="$ďđ" + app:popupKeyboard="@xml/keys_popup_template" + app:topSmallNumber="$" /> - - - - + app:popupCharacters="%₣" + app:popupKeyboard="@xml/keys_popup_template" + app:topSmallNumber="%" /> + + + + + app:popupCharacters=")ĺľł" + app:popupKeyboard="@xml/keys_popup_template" + app:topSmallNumber=")" /> - + app:popupCharacters="*źžż" + app:popupKeyboard="@xml/keys_popup_template" + app:topSmallNumber="*" /> + - - + app:popupCharacters="'çčć¢" + app:popupKeyboard="@xml/keys_popup_template" + app:topSmallNumber="'" /> + + - + app:popupCharacters="!ňńñ" + app:popupKeyboard="@xml/keys_popup_template" + app:topSmallNumber="!" /> + SHIFT_ON_ONE_CHAR + shiftState = when (shiftState) { + ItemMainKeyboard.SHIFT_OFF -> ItemMainKeyboard.SHIFT_ON_ONE_CHAR + ItemMainKeyboard.SHIFT_ON_ONE_CHAR -> ItemMainKeyboard.SHIFT_OFF + else -> ItemMainKeyboard.SHIFT_OFF + } + assertEquals(ItemMainKeyboard.SHIFT_ON_ONE_CHAR, shiftState) + + // 2nd press (rapid tap) -> SHIFT_ON_PERMANENT (Caps Lock) + val isDoubleTap = true + if (isDoubleTap) { + shiftState = ItemMainKeyboard.SHIFT_ON_PERMANENT + } + assertEquals(ItemMainKeyboard.SHIFT_ON_PERMANENT, shiftState) + + // 3rd press -> SHIFT_OFF + shiftState = when (shiftState) { + ItemMainKeyboard.SHIFT_ON_PERMANENT -> ItemMainKeyboard.SHIFT_OFF + else -> ItemMainKeyboard.SHIFT_ON_ONE_CHAR + } + assertEquals(ItemMainKeyboard.SHIFT_OFF, shiftState) + } + + @Test + fun testDoubleSpaceToPeriodCondition() { + val textBefore1 = "hello " + val canInsertPeriod1 = textBefore1.endsWith(" ") && !textBefore1.endsWith(". ") + assertTrue(canInsertPeriod1) + + val textBefore2 = "hello. " + val canInsertPeriod2 = textBefore2.endsWith(" ") && !textBefore2.endsWith(". ") + assertFalse(canInsertPeriod2) + + val textBefore3 = " " + val canInsertPeriod3 = textBefore3.endsWith(" ") && !textBefore3.endsWith(". ") + assertTrue(canInsertPeriod3) + } + + @Test + fun testDeleteWordsBeforeCursorLogic() { + val sampleText = "The quick brown fox jumps" + val wordsToDelete = 2 // should delete "fox jumps" and the preceding space + + var remainingWords = wordsToDelete + var deleteLen = 0 + var inWord = false + + for (i in sampleText.length - 1 downTo 0) { + val ch = sampleText[i] + if (ch.isWhitespace() || !ch.isLetterOrDigit()) { + if (inWord) { + remainingWords-- + if (remainingWords <= 0) { + deleteLen++ + break + } + inWord = false + } + } else { + inWord = true + } + deleteLen++ + } + + val remainingText = sampleText.dropLast(deleteLen) + assertEquals("The quick brown", remainingText) + } + + @Test + fun testHslHsvConversion() { + val originalHsl = floatArrayOf(200f, 0.5f, 0.5f) + val hsv = com.frogobox.libkeyboard.common.ext.hsl2hsv(originalHsl) + val convertedHsl = com.frogobox.libkeyboard.common.ext.hsv2hsl(hsv) + assertEquals(originalHsl[0], convertedHsl[0], 0.02f) + assertEquals(originalHsl[1], convertedHsl[1], 0.02f) + assertEquals(originalHsl[2], convertedHsl[2], 0.02f) + } + + @Test + fun testConstantsIntegrity() { + assertEquals(0, BaseKeyboardIME.KEYBOARD_LETTERS) + assertEquals(1, BaseKeyboardIME.KEYBOARD_SYMBOLS) + assertEquals(2, BaseKeyboardIME.KEYBOARD_SYMBOLS_SHIFT) + assertEquals(3, BaseKeyboardIME.KEYBOARD_NUMBER) + assertEquals(350L, BaseKeyboardIME.DOUBLE_SPACE_PERIOD_TIMEOUT) + } +} diff --git a/frogo-keyboard/src/test/java/com/frogobox/libkeyboard/KeyboardLayoutLogicTest.kt b/frogo-keyboard/src/test/java/com/frogobox/libkeyboard/KeyboardLayoutLogicTest.kt new file mode 100644 index 0000000..e3ab8d1 --- /dev/null +++ b/frogo-keyboard/src/test/java/com/frogobox/libkeyboard/KeyboardLayoutLogicTest.kt @@ -0,0 +1,52 @@ +package com.frogobox.libkeyboard + +import com.frogobox.libkeyboard.common.sound.MechanicalSoundType +import com.frogobox.libkeyboard.ui.emoji.EmojiCategoryType +import com.frogobox.libkeyboard.ui.emoji.getEmojiCategory +import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class KeyboardLayoutLogicTest { + + @Test + fun testMechanicalSoundTypeMapping() { + assertEquals(MechanicalSoundType.CHERRY_MX_BLUE, MechanicalSoundType.fromId("cherry_mx_blue")) + assertEquals(MechanicalSoundType.CHERRY_MX_BROWN, MechanicalSoundType.fromId("cherry_mx_brown")) + assertEquals(MechanicalSoundType.CHERRY_MX_RED, MechanicalSoundType.fromId("cherry_mx_red")) + assertEquals(MechanicalSoundType.TYPEWRITER, MechanicalSoundType.fromId("typewriter")) + assertEquals(MechanicalSoundType.SYSTEM_CLICK, MechanicalSoundType.fromId("system")) + assertEquals(MechanicalSoundType.OFF, MechanicalSoundType.fromId("off")) + // Fallback on unknown id + assertEquals(MechanicalSoundType.CHERRY_MX_BLUE, MechanicalSoundType.fromId("unknown_switch")) + } + + @Test + fun testEmojiCategoriesMapping() { + val categories = getEmojiCategory() + assertNotNull(categories) + assertEquals(EmojiCategoryType.entries.size, categories.size) + assertTrue(categories.any { it.name == EmojiCategoryType.SMILEYS_EMOTION.name }) + assertTrue(categories.any { it.name == EmojiCategoryType.PEOPLE_BODY.name }) + } + + @Test + fun testKeycodeConstants() { + assertEquals(-1, ItemMainKeyboard.KEYCODE_SHIFT) + assertEquals(-2, ItemMainKeyboard.KEYCODE_MODE_CHANGE) + assertEquals(-4, ItemMainKeyboard.KEYCODE_ENTER) + assertEquals(-5, ItemMainKeyboard.KEYCODE_DELETE) + assertEquals(32, ItemMainKeyboard.KEYCODE_SPACE) + assertEquals(-6, ItemMainKeyboard.KEYCODE_EMOJI) + assertEquals(-7, ItemMainKeyboard.KEYCODE_TAB) + } + + @Test + fun testAdjustCaseLogic() { + val label = "a" + val upper = label.uppercase() + assertEquals("A", upper) + } +} From 0824b07772cd21b822922c40ecf273c2dbca83bc Mon Sep 17 00:00:00 2001 From: amirisback Date: Sat, 19 Sep 2026 23:47:34 +0700 Subject: [PATCH 2/8] feat: implement DataApiService and DataApiRepository for remote data fetching --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 3 + .../appkeyboard/common/core/Resource.kt | 13 + .../appkeyboard/data/remote/DataApiService.kt | 15 + .../data/remote/model/DataApiResponse.kt | 29 + .../data/remote/model/DataItemResponse.kt | 78 +++ .../frogobox/appkeyboard/di/NetworkModule.kt | 26 +- .../appkeyboard/di/RepositoryModule.kt | 16 +- .../repository/data/DataApiRepository.kt | 16 + .../repository/data/DataApiRepositoryImpl.kt | 73 ++ .../ui/keyboard/form/FormKeyboard.kt | 2 +- .../templatetext/TemplateTextKeyboard.kt | 34 +- .../appkeyboard/ui/test/DataApiUiState.kt | 19 + .../appkeyboard/ui/test/TestActivity.kt | 4 + .../appkeyboard/ui/test/TestScreen.kt | 629 +++++++++++++++++- .../appkeyboard/ui/test/TestViewModel.kt | 47 +- .../main/res/xml/network_security_config.xml | 9 + .../KeyboardFeaturesRedesignTest.kt | 10 + .../repository/DataApiRepositoryTest.kt | 267 ++++++++ .../ui/test/DataApiViewModelTest.kt | 209 ++++++ 20 files changed, 1464 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/com/frogobox/appkeyboard/common/core/Resource.kt create mode 100644 app/src/main/java/com/frogobox/appkeyboard/data/remote/DataApiService.kt create mode 100644 app/src/main/java/com/frogobox/appkeyboard/data/remote/model/DataApiResponse.kt create mode 100644 app/src/main/java/com/frogobox/appkeyboard/data/remote/model/DataItemResponse.kt create mode 100644 app/src/main/java/com/frogobox/appkeyboard/repository/data/DataApiRepository.kt create mode 100644 app/src/main/java/com/frogobox/appkeyboard/repository/data/DataApiRepositoryImpl.kt create mode 100644 app/src/main/java/com/frogobox/appkeyboard/ui/test/DataApiUiState.kt create mode 100644 app/src/main/res/xml/network_security_config.xml create mode 100644 app/src/test/java/com/frogobox/appkeyboard/repository/DataApiRepositoryTest.kt create mode 100644 app/src/test/java/com/frogobox/appkeyboard/ui/test/DataApiViewModelTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 89e6f12..cd13613 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -141,5 +141,6 @@ dependencies { ksp(libs.androidx.room.compiler) ksp(libs.androidx.lifecycle.compiler) testImplementation(libs.junit) + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 30cf068..abf26e6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,11 +2,14 @@ + + { + data class Success(val data: T) : Resource() + data class Error(val message: String, val cause: Throwable? = null, val code: Int? = null) : Resource() + data object Loading : Resource() +} diff --git a/app/src/main/java/com/frogobox/appkeyboard/data/remote/DataApiService.kt b/app/src/main/java/com/frogobox/appkeyboard/data/remote/DataApiService.kt new file mode 100644 index 0000000..1bdfdf7 --- /dev/null +++ b/app/src/main/java/com/frogobox/appkeyboard/data/remote/DataApiService.kt @@ -0,0 +1,15 @@ +package com.frogobox.appkeyboard.data.remote + +import com.frogobox.appkeyboard.data.remote.model.DataApiResponse +import retrofit2.Response +import retrofit2.http.GET + +/** + * Retrofit network service interface for fetching data from remote endpoint. + */ +interface DataApiService { + + @GET("api/data.json") + suspend fun getData(): Response + +} diff --git a/app/src/main/java/com/frogobox/appkeyboard/data/remote/model/DataApiResponse.kt b/app/src/main/java/com/frogobox/appkeyboard/data/remote/model/DataApiResponse.kt new file mode 100644 index 0000000..6275768 --- /dev/null +++ b/app/src/main/java/com/frogobox/appkeyboard/data/remote/model/DataApiResponse.kt @@ -0,0 +1,29 @@ +package com.frogobox.appkeyboard.data.remote.model + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +/** + * Root data model for remote API response from /api/data.json. + * Annotated with @Keep and @SerializedName for safe serialization/deserialization. + */ +@Keep +data class DataApiResponse( + @SerializedName("code") + val code: Int? = null, + + @SerializedName("status") + val status: String? = null, + + @SerializedName("message") + val message: String? = null, + + @SerializedName("total") + val total: Int? = null, + + @SerializedName("lastUpdated") + val lastUpdated: String? = null, + + @SerializedName("data") + val data: List? = null +) diff --git a/app/src/main/java/com/frogobox/appkeyboard/data/remote/model/DataItemResponse.kt b/app/src/main/java/com/frogobox/appkeyboard/data/remote/model/DataItemResponse.kt new file mode 100644 index 0000000..3db6536 --- /dev/null +++ b/app/src/main/java/com/frogobox/appkeyboard/data/remote/model/DataItemResponse.kt @@ -0,0 +1,78 @@ +package com.frogobox.appkeyboard.data.remote.model + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +/** + * Data model for individual items returned from remote API. + * Annotated with @Keep and @SerializedName to ensure ProGuard obfuscation safety. + */ +@Keep +data class DataItemResponse( + @SerializedName("id") + val id: Int? = null, + + @SerializedName("title") + val title: String? = null, + + @SerializedName("body") + val body: String? = null, + + @SerializedName("description") + val description: String? = null, + + @SerializedName("category") + val category: String? = null, + + @SerializedName("createdAt") + val createdAt: String? = null, + + @SerializedName("isActive") + val isActive: Boolean? = null, + + @SerializedName("rowIndex") + val rowIndex: Int? = null, + + @SerializedName("isVideo") + val isVideo: Boolean? = null, + + @SerializedName("fileSize") + val fileSize: String? = null, + + @SerializedName("caption") + val caption: String? = null, + + @SerializedName("uploaderName") + val uploaderName: String? = null, + + @SerializedName("uploadTimestamp") + val uploadTimestamp: String? = null, + + @SerializedName("driveLink") + val driveLink: String? = null, + + @SerializedName("thumbnailUrl") + val thumbnailUrl: String? = null +) { + val displayIndex: Int + get() = rowIndex ?: id ?: 0 + + val displayTitle: String + get() = caption?.takeIf { it.isNotBlank() } + ?: title?.takeIf { it.isNotBlank() } + ?: uploaderName?.takeIf { it.isNotBlank() } + ?: "Item #$displayIndex" + + val displaySubtitle: String? + get() = uploaderName?.takeIf { it.isNotBlank() } + ?: category?.takeIf { it.isNotBlank() } + + val displayBody: String? + get() = body?.takeIf { it.isNotBlank() } + ?: description?.takeIf { it.isNotBlank() } + ?: caption?.takeIf { it.isNotBlank() } + + val displayTimestamp: String? + get() = uploadTimestamp?.takeIf { it.isNotBlank() } + ?: createdAt?.takeIf { it.isNotBlank() } +} diff --git a/app/src/main/java/com/frogobox/appkeyboard/di/NetworkModule.kt b/app/src/main/java/com/frogobox/appkeyboard/di/NetworkModule.kt index a014346..ecda194 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/di/NetworkModule.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/di/NetworkModule.kt @@ -1,22 +1,30 @@ package com.frogobox.appkeyboard.di +import android.content.Context +import com.frogobox.appkeyboard.data.remote.ApiService +import com.frogobox.appkeyboard.data.remote.DataApiService import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton /** - * Created by Faisal Amir on 06/01/23 - * ----------------------------------------- - * E-mail : faisalamircs@gmail.com - * Github : github.com/amirisback - * ----------------------------------------- - * Copyright (C) Frogobox ID / amirisback - * All rights reserved + * Hilt module providing network dependencies and services. */ - - @Module @InstallIn(SingletonComponent::class) class NetworkModule { + companion object { + const val BASE_URL = "http://192.168.100.6:3000/" + } + + @Provides + @Singleton + fun provideDataApiService(@ApplicationContext context: Context): DataApiService { + return ApiService.create(context, BASE_URL) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/frogobox/appkeyboard/di/RepositoryModule.kt b/app/src/main/java/com/frogobox/appkeyboard/di/RepositoryModule.kt index 5696078..af90583 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/di/RepositoryModule.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/di/RepositoryModule.kt @@ -2,21 +2,17 @@ package com.frogobox.appkeyboard.di import com.frogobox.appkeyboard.repository.autotext.AutoTextRepository import com.frogobox.appkeyboard.repository.autotext.AutoTextRepositoryImpl +import com.frogobox.appkeyboard.repository.data.DataApiRepository +import com.frogobox.appkeyboard.repository.data.DataApiRepositoryImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton /** - * Created by Faisal Amir on 06/01/23 - * ----------------------------------------- - * E-mail : faisalamircs@gmail.com - * Github : github.com/amirisback - * ----------------------------------------- - * Copyright (C) Frogobox ID / amirisback - * All rights reserved + * Hilt module binding repository implementations to their interfaces. */ - @Module(includes = [ NetworkModule::class, ServiceModule::class, @@ -28,4 +24,8 @@ abstract class RepositoryModule { @Binds abstract fun getAutoTextRepository(repository: AutoTextRepositoryImpl): AutoTextRepository + @Binds + @Singleton + abstract fun bindDataApiRepository(repository: DataApiRepositoryImpl): DataApiRepository + } \ No newline at end of file diff --git a/app/src/main/java/com/frogobox/appkeyboard/repository/data/DataApiRepository.kt b/app/src/main/java/com/frogobox/appkeyboard/repository/data/DataApiRepository.kt new file mode 100644 index 0000000..85510c9 --- /dev/null +++ b/app/src/main/java/com/frogobox/appkeyboard/repository/data/DataApiRepository.kt @@ -0,0 +1,16 @@ +package com.frogobox.appkeyboard.repository.data + +import com.frogobox.appkeyboard.common.core.Resource +import com.frogobox.appkeyboard.data.remote.model.DataApiResponse +import kotlinx.coroutines.flow.Flow + +/** + * Repository interface defining operations for fetching remote API data. + */ +interface DataApiRepository { + + fun fetchDataStream(): Flow> + + suspend fun fetchData(): Resource + +} diff --git a/app/src/main/java/com/frogobox/appkeyboard/repository/data/DataApiRepositoryImpl.kt b/app/src/main/java/com/frogobox/appkeyboard/repository/data/DataApiRepositoryImpl.kt new file mode 100644 index 0000000..3e8e5c3 --- /dev/null +++ b/app/src/main/java/com/frogobox/appkeyboard/repository/data/DataApiRepositoryImpl.kt @@ -0,0 +1,73 @@ +package com.frogobox.appkeyboard.repository.data + +import com.frogobox.appkeyboard.common.core.Resource +import com.frogobox.appkeyboard.data.remote.DataApiService +import com.frogobox.appkeyboard.data.remote.model.DataApiResponse +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.withContext +import java.io.IOException +import java.net.ConnectException +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Implementation of DataApiRepository executing asynchronous network calls on Dispatchers.IO. + */ +@Singleton +class DataApiRepositoryImpl( + private val apiService: DataApiService, + private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO +) : DataApiRepository { + + @Inject + constructor(apiService: DataApiService) : this(apiService, Dispatchers.IO) + + override fun fetchDataStream(): Flow> = flow { + emit(Resource.Loading) + val result = executeFetch() + emit(result) + }.catch { e -> + emit(Resource.Error(message = formatErrorMessage(e), cause = e)) + }.flowOn(ioDispatcher) + + override suspend fun fetchData(): Resource = withContext(ioDispatcher) { + executeFetch() + } + + private suspend fun executeFetch(): Resource { + return try { + val response = apiService.getData() + if (response.isSuccessful) { + val body = response.body() + if (body != null) { + Resource.Success(body) + } else { + Resource.Error(message = "Empty response body from server", code = response.code()) + } + } else { + val errorBody = response.errorBody()?.string()?.takeIf { it.isNotBlank() } + val errorMsg = errorBody ?: response.message().takeIf { it.isNotBlank() } ?: "HTTP error" + Resource.Error(message = "HTTP ${response.code()}: $errorMsg", code = response.code()) + } + } catch (e: Exception) { + Resource.Error(message = formatErrorMessage(e), cause = e) + } + } + + private fun formatErrorMessage(throwable: Throwable): String { + return when (throwable) { + is ConnectException -> "Connection refused. Please ensure the local server is running at http://192.168.100.6:3000." + is SocketTimeoutException -> "Connection timed out while communicating with the server." + is UnknownHostException -> "Unable to resolve host 192.168.100.6. Check your Wi-Fi network." + is IOException -> throwable.localizedMessage ?: "Network I/O error occurred." + else -> throwable.localizedMessage ?: "Unexpected error occurred." + } + } +} diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboard.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboard.kt index 744fe76..49924dd 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboard.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboard.kt @@ -24,7 +24,7 @@ class FormKeyboard( attrs: AttributeSet?, ) : BaseKeyboard(context, attrs) { - private val destinationKeyboard = MovieKeyboard(context, attrs) + private val destinationKeyboard by lazy { MovieKeyboard(context, attrs) } override fun setupViewBinding(inflater: LayoutInflater, parent: LinearLayout): KeyboardFormBinding { return KeyboardFormBinding.inflate(LayoutInflater.from(context), this, true) diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboard.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboard.kt index 1ef4a8e..ed91526 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboard.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboard.kt @@ -34,15 +34,17 @@ class TemplateTextKeyboard( attrs: AttributeSet?, ) : BaseKeyboard(context, attrs) { - private var currentType: KeyboardFeatureType = TEMPLATE_TEXT_GAME + companion object { + val CATEGORIES = listOf( + Pair(TEMPLATE_TEXT_GAME, Pair("🎮", "Game")), + Pair(TEMPLATE_TEXT_APP, Pair("📱", "App")), + Pair(TEMPLATE_TEXT_SALE, Pair("💰", "Sale")), + Pair(TEMPLATE_TEXT_GREETING, Pair("👋", "Greeting")), + Pair(TEMPLATE_TEXT_LOVE, Pair("❤️", "Love")) + ) + } - private val categories = listOf( - Pair(TEMPLATE_TEXT_GAME, Pair("🎮", "Game")), - Pair(TEMPLATE_TEXT_APP, Pair("📱", "App")), - Pair(TEMPLATE_TEXT_SALE, Pair("💰", "Sale")), - Pair(TEMPLATE_TEXT_GREETING, Pair("👋", "Greeting")), - Pair(TEMPLATE_TEXT_LOVE, Pair("❤️", "Love")) - ) + private var currentType: KeyboardFeatureType? = null override fun setupViewBinding(inflater: LayoutInflater, parent: LinearLayout): KeyboardTemplateTextBinding { return KeyboardTemplateTextBinding.inflate(LayoutInflater.from(context), this, true) @@ -50,23 +52,25 @@ class TemplateTextKeyboard( override fun initUI() { super.initUI() - setupCategories() - setupContent(currentType) + val type = currentType ?: TEMPLATE_TEXT_GAME + currentType = type + setupCategories(type) + setupContent(type) } fun setupTemplateTextType(templateTextType: KeyboardFeatureType) { this.currentType = templateTextType - setupCategories() + setupCategories(templateTextType) setupContent(templateTextType) } - private fun setupCategories() { - val categoryData = categories.map { (type, meta) -> + private fun setupCategories(selectedType: KeyboardFeatureType) { + val categoryData = CATEGORIES.map { (type, meta) -> TemplateCategoryItem( type = type, icon = meta.first, title = meta.second, - isSelected = type == currentType + isSelected = type == selectedType ) } @@ -111,7 +115,7 @@ class TemplateTextKeyboard( ) { if (currentType != data.type) { currentType = data.type - setupCategories() + setupCategories(data.type) setupContent(data.type) } } diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/test/DataApiUiState.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/test/DataApiUiState.kt new file mode 100644 index 0000000..345e6f9 --- /dev/null +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/test/DataApiUiState.kt @@ -0,0 +1,19 @@ +package com.frogobox.appkeyboard.ui.test + +import androidx.annotation.Keep +import com.frogobox.appkeyboard.data.remote.model.DataItemResponse + +/** + * UI State machine representing Remote API fetch lifecycle in Test Playground. + */ +@Keep +sealed interface DataApiUiState { + data object Idle : DataApiUiState + data object Loading : DataApiUiState + data class Success( + val total: Int, + val lastUpdatedWib: String, + val items: List + ) : DataApiUiState + data class Error(val message: String) : DataApiUiState +} diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestActivity.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestActivity.kt index 03f0348..30a39f6 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestActivity.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestActivity.kt @@ -82,6 +82,7 @@ class TestActivity : BaseComposeActivity() { val metrics by viewModel.metrics.collectAsState() val activeTab by viewModel.activeTab.collectAsState() val autoTextList by viewModel.autoTextList.collectAsState() + val remoteApiUiState by viewModel.remoteApiUiState.collectAsState() TestScreen( sandboxText = sandboxText, @@ -89,12 +90,15 @@ class TestActivity : BaseComposeActivity() { activeTab = activeTab, autoTextList = autoTextList, isKeyboardActive = isUsingFrogoKeyboard, + remoteApiUiState = remoteApiUiState, dummyOptions = dummyOptions, onTextChanged = viewModel::onTextChanged, onInsertText = viewModel::onInsertText, onClearText = viewModel::onClearText, onResetTimer = viewModel::onResetTimer, onTabSelected = viewModel::onTabSelected, + onFetchRemoteData = viewModel::fetchRemoteData, + onResetRemoteData = viewModel::resetRemoteData, onChangeKeyboard = { (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager).showInputMethodPicker() }, diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestScreen.kt index 7f82c6b..34946a1 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestScreen.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestScreen.kt @@ -29,10 +29,16 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Notes import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.CloudDownload +import androidx.compose.material.icons.filled.CloudOff +import androidx.compose.material.icons.filled.CloudQueue import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Inbox import androidx.compose.material.icons.filled.Keyboard +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.PlayCircle import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings @@ -45,6 +51,7 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api @@ -74,14 +81,17 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.frogobox.appkeyboard.data.remote.model.DataItemResponse import com.frogobox.appkeyboard.model.AutoTextEntity import com.frogobox.appkeyboard.ui.theme.compose.FrogoKeyboardTheme import com.frogobox.appkeyboard.ui.theme.compose.FrogoPrimary @@ -101,12 +111,15 @@ fun TestScreen( activeTab: Int, autoTextList: List, isKeyboardActive: Boolean, + remoteApiUiState: DataApiUiState = DataApiUiState.Idle, dummyOptions: List = emptyList(), onTextChanged: (String) -> Unit, onInsertText: (String) -> Unit, onClearText: () -> Unit, onResetTimer: () -> Unit, onTabSelected: (Int) -> Unit, + onFetchRemoteData: () -> Unit = {}, + onResetRemoteData: () -> Unit = {}, onChangeKeyboard: () -> Unit, onGoToSettings: () -> Unit, onBackClick: (() -> Unit)? = null, @@ -160,7 +173,7 @@ fun TestScreen( // 2. Real-time Live Typing Metrics Card LiveMetricsCard(metrics = metrics) - // 3. Tab Navigation Row + // 3. Tab Navigation Row (4 Tabs) PrimaryTabRow( selectedTabIndex = activeTab, containerColor = MaterialTheme.colorScheme.surface, @@ -184,6 +197,12 @@ fun TestScreen( text = { Text("AutoText", fontWeight = FontWeight.SemiBold) }, icon = { Icon(Icons.AutoMirrored.Filled.Notes, contentDescription = null, modifier = Modifier.size(20.dp)) } ) + Tab( + selected = activeTab == 3, + onClick = { onTabSelected(3) }, + text = { Text("Remote API", fontWeight = FontWeight.SemiBold) }, + icon = { Icon(Icons.Default.CloudDownload, contentDescription = null, modifier = Modifier.size(20.dp)) } + ) } // 4. Tab Body Content @@ -227,6 +246,16 @@ fun TestScreen( onTabSelected(0) } ) + 3 -> RemoteApiTabContent( + uiState = remoteApiUiState, + onFetchClicked = onFetchRemoteData, + onResetClicked = onResetRemoteData, + onInsertToSandbox = { text -> + onInsertText(text) + Toast.makeText(context, "Inserted remote data to sandbox", Toast.LENGTH_SHORT).show() + onTabSelected(0) + } + ) } Spacer(modifier = Modifier.height(16.dp)) @@ -763,3 +792,601 @@ private fun AutoTextTabContent( } } } + +/** + * 4. Remote API Tab Content Component + */ +@Composable +private fun RemoteApiTabContent( + uiState: DataApiUiState, + onFetchClicked: () -> Unit, + onResetClicked: () -> Unit, + onInsertToSandbox: (String) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + // 1. Endpoint & Status Header Banner + ApiHeaderBanner( + uiState = uiState, + onFetchClicked = onFetchClicked, + onResetClicked = onResetClicked + ) + + // 2. State-driven Body + when (uiState) { + is DataApiUiState.Idle -> ApiIdleView(onFetchClicked = onFetchClicked) + is DataApiUiState.Loading -> ApiLoadingView() + is DataApiUiState.Error -> ApiErrorView( + errorMessage = uiState.message, + onRetryClicked = onFetchClicked + ) + is DataApiUiState.Success -> { + if (uiState.items.isEmpty()) { + ApiEmptyView(onRefreshClicked = onFetchClicked) + } else { + ApiSuccessListView( + items = uiState.items, + onInsertToSandbox = onInsertToSandbox + ) + } + } + } + } +} + +@Composable +private fun ApiHeaderBanner( + uiState: DataApiUiState, + onFetchClicked: () -> Unit, + onResetClicked: () -> Unit, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.35f)), + elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "🌐 Remote Data Ingestion", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + Box( + modifier = Modifier + .clip(RoundedCornerShape(6.dp)) + .background(FrogoStatusSuccess.copy(alpha = 0.12f)) + .padding(horizontal = 8.dp, vertical = 3.dp) + ) { + Text( + text = "● Online: Port 3000", + fontSize = 11.sp, + fontWeight = FontWeight.SemiBold, + color = FrogoStatusSuccess + ) + } + } + + // Monospace endpoint badge + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant) + .padding(horizontal = 10.dp, vertical = 6.dp) + ) { + Text( + text = "🔗 http://192.168.100.6:3000/api/data.json", + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + // Metadata summary when Success + if (uiState is DataApiUiState.Success) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(6.dp)) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)) + .padding(horizontal = 8.dp, vertical = 3.dp) + ) { + Text( + text = "🏷️ ${uiState.total} Items Available", + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + } + + Text( + text = "🕒 ${uiState.lastUpdatedWib}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + + // Primary action row + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button( + onClick = onFetchClicked, + modifier = Modifier + .weight(1f) + .height(48.dp), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors(containerColor = FrogoPrimary) + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "Fetch Live Data", + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = if (uiState is DataApiUiState.Success) "Refresh Data" else "Fetch Live Data", + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.SemiBold + ) + } + + if (uiState !is DataApiUiState.Idle) { + OutlinedButton( + onClick = onResetClicked, + modifier = Modifier.height(48.dp), + shape = RoundedCornerShape(12.dp) + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Reset State", + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = "Reset", + style = MaterialTheme.typography.labelLarge + ) + } + } + } + } + } +} + +@Composable +private fun ApiIdleView( + onFetchClicked: () -> Unit, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.25f)) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Box( + modifier = Modifier + .size(56.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.CloudQueue, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(32.dp) + ) + } + + Text( + text = "No Data Fetched Yet", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + + Text( + text = "Tap 'Fetch Live Data' to load dynamic content from the local server (http://192.168.100.6:3000/api/data.json).", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + + Button( + onClick = onFetchClicked, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors(containerColor = FrogoPrimary) + ) { + Icon( + imageVector = Icons.Default.CloudDownload, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Fetch Data Now", fontWeight = FontWeight.SemiBold) + } + } + } +} + +@Composable +private fun ApiLoadingView(modifier: Modifier = Modifier) { + Card( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.25f)) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + CircularProgressIndicator( + modifier = Modifier.size(36.dp), + strokeWidth = 3.dp, + color = FrogoPrimary + ) + Text( + text = "Connecting to server & fetching data...", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = "Executing on Dispatchers.IO via Kotlin Coroutines & Flow", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +@Composable +private fun ApiErrorView( + errorMessage: String, + onRetryClicked: () -> Unit, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.25f) + ), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.error.copy(alpha = 0.4f)) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Box( + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.error.copy(alpha = 0.15f)), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.CloudOff, + contentDescription = "Error Icon", + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(20.dp) + ) + } + + Text( + text = "Failed to Fetch Remote Data", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.error + ) + } + + Text( + text = errorMessage, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.surface) + .padding(10.dp) + ) { + Text( + text = "Troubleshooting:\n1. Verify Node.js server is running on port 3000 at 192.168.100.6.\n2. Ensure device and server are on the same local Wi-Fi subnet.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Button( + onClick = onRetryClicked, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error) + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Retry Connection", fontWeight = FontWeight.SemiBold) + } + } + } +} + +@Composable +private fun ApiEmptyView( + onRefreshClicked: () -> Unit, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.25f)) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + imageVector = Icons.Default.Inbox, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(40.dp) + ) + Text( + text = "Server returned 0 records.", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + Button( + onClick = onRefreshClicked, + modifier = Modifier.height(48.dp), + shape = RoundedCornerShape(12.dp) + ) { + Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(18.dp)) + Spacer(modifier = Modifier.width(6.dp)) + Text("Refresh") + } + } + } +} + +@Composable +private fun ApiSuccessListView( + items: List, + onInsertToSandbox: (String) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items.forEach { item -> + ApiDataItemCard( + item = item, + onInsertToSandbox = onInsertToSandbox + ) + } + } +} + +@Composable +private fun ApiDataItemCard( + item: DataItemResponse, + onInsertToSandbox: (String) -> Unit, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + + Card( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)), + elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + // Badges row + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + Text( + text = "#ROW ${item.displayIndex}", + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .background( + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f), + shape = RoundedCornerShape(6.dp) + ) + .padding(horizontal = 6.dp, vertical = 2.dp) + ) + if (item.isVideo == true) { + Text( + text = "🎬 Video", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .background( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(6.dp) + ) + .padding(horizontal = 6.dp, vertical = 2.dp) + ) + } + } + Text( + text = item.fileSize ?: "-", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + // Media & Caption row + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.Top + ) { + // Media Icon / Thumbnail Box + Box( + modifier = Modifier + .size(56.dp) + .clip(RoundedCornerShape(10.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.PlayCircle, + contentDescription = "Media Preview", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(28.dp) + ) + } + + // Text Details Column + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(3.dp) + ) { + Text( + text = item.displayTitle, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + item.displaySubtitle?.let { subtitle -> + Text( + text = "👤 $subtitle", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + item.displayTimestamp?.let { timestamp -> + Text( + text = "🕒 $timestamp", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f) + ) + } + } + } + + // Action Buttons Row + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button( + onClick = { + val textToInsert = item.displayBody ?: item.displayTitle + onInsertToSandbox(textToInsert) + }, + modifier = Modifier + .weight(1f) + .height(48.dp), + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary) + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text("Insert Sandbox", style = MaterialTheme.typography.labelMedium) + } + + if (!item.driveLink.isNullOrEmpty()) { + OutlinedButton( + onClick = { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setPrimaryClip(ClipData.newPlainText("Drive Link", item.driveLink)) + Toast.makeText(context, "Drive link copied!", Toast.LENGTH_SHORT).show() + }, + modifier = Modifier.height(48.dp), + shape = RoundedCornerShape(10.dp) + ) { + Icon( + imageVector = Icons.Default.ContentCopy, + contentDescription = "Copy Drive Link", + modifier = Modifier.size(16.dp) + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestViewModel.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestViewModel.kt index 67dfd24..accb896 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestViewModel.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestViewModel.kt @@ -2,24 +2,31 @@ package com.frogobox.appkeyboard.ui.test import androidx.lifecycle.viewModelScope import com.frogobox.appkeyboard.common.base.BaseViewModel +import com.frogobox.appkeyboard.common.core.Resource import com.frogobox.appkeyboard.model.AutoTextEntity import com.frogobox.appkeyboard.model.AutoTextLabelType import com.frogobox.appkeyboard.repository.autotext.AutoTextRepository +import com.frogobox.appkeyboard.repository.data.DataApiRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.launch +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone import javax.inject.Inject /** * Created by Faisal Amir on 24/10/22 - * Redesigned for TASK-010 by Tim Mobile + * Redesigned for TASK-010 and TASK-015 by Tim Mobile */ @HiltViewModel class TestViewModel @Inject constructor( - private val repository: AutoTextRepository + private val repository: AutoTextRepository, + private val dataApiRepository: DataApiRepository ) : BaseViewModel() { private val _sandboxText = MutableStateFlow("") @@ -34,6 +41,9 @@ class TestViewModel @Inject constructor( private val _autoTextList = MutableStateFlow>(emptyList()) val autoTextList: StateFlow> = _autoTextList.asStateFlow() + private val _remoteApiUiState = MutableStateFlow(DataApiUiState.Idle) + val remoteApiUiState: StateFlow = _remoteApiUiState.asStateFlow() + private var typingStartTimestamp: Long = 0L init { @@ -121,4 +131,37 @@ class TestViewModel @Inject constructor( fun onTabSelected(tabIndex: Int) { _activeTab.value = tabIndex } + + fun fetchRemoteData() { + viewModelScope.launch { + dataApiRepository.fetchDataStream().collect { resource -> + when (resource) { + is Resource.Loading -> { + _remoteApiUiState.value = DataApiUiState.Loading + } + is Resource.Success -> { + val items = resource.data.data ?: emptyList() + val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).apply { + timeZone = TimeZone.getTimeZone("Asia/Jakarta") + } + val nowWib = "${sdf.format(Date())} WIB" + _remoteApiUiState.value = DataApiUiState.Success( + total = resource.data.total ?: items.size, + lastUpdatedWib = resource.data.lastUpdated ?: nowWib, + items = items + ) + } + is Resource.Error -> { + _remoteApiUiState.value = DataApiUiState.Error( + message = resource.message + ) + } + } + } + } + } + + fun resetRemoteData() { + _remoteApiUiState.value = DataApiUiState.Idle + } } \ No newline at end of file diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..853c722 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,9 @@ + + + + 192.168.100.6 + 10.0.2.2 + localhost + + + diff --git a/app/src/test/java/com/frogobox/appkeyboard/KeyboardFeaturesRedesignTest.kt b/app/src/test/java/com/frogobox/appkeyboard/KeyboardFeaturesRedesignTest.kt index 0c9a1e6..89cc2f6 100644 --- a/app/src/test/java/com/frogobox/appkeyboard/KeyboardFeaturesRedesignTest.kt +++ b/app/src/test/java/com/frogobox/appkeyboard/KeyboardFeaturesRedesignTest.kt @@ -2,6 +2,7 @@ package com.frogobox.appkeyboard import com.frogobox.appkeyboard.model.KeyboardFeatureType import com.frogobox.appkeyboard.ui.keyboard.templatetext.TemplateCategoryItem +import com.frogobox.appkeyboard.ui.keyboard.templatetext.TemplateTextKeyboard import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -10,6 +11,15 @@ import java.net.URLEncoder class KeyboardFeaturesRedesignTest { + @Test + fun testTemplateCategoriesStaticInitialization() { + assertFalse(TemplateTextKeyboard.CATEGORIES.isEmpty()) + assertEquals(5, TemplateTextKeyboard.CATEGORIES.size) + assertEquals(KeyboardFeatureType.TEMPLATE_TEXT_GAME, TemplateTextKeyboard.CATEGORIES[0].first) + assertEquals("🎮", TemplateTextKeyboard.CATEGORIES[0].second.first) + assertEquals("Game", TemplateTextKeyboard.CATEGORIES[0].second.second) + } + @Test fun testTemplateCategoriesCoverage() { val categories = listOf( diff --git a/app/src/test/java/com/frogobox/appkeyboard/repository/DataApiRepositoryTest.kt b/app/src/test/java/com/frogobox/appkeyboard/repository/DataApiRepositoryTest.kt new file mode 100644 index 0000000..4674b49 --- /dev/null +++ b/app/src/test/java/com/frogobox/appkeyboard/repository/DataApiRepositoryTest.kt @@ -0,0 +1,267 @@ +package com.frogobox.appkeyboard.repository + +import com.frogobox.appkeyboard.common.core.Resource +import com.frogobox.appkeyboard.data.remote.DataApiService +import com.frogobox.appkeyboard.data.remote.model.DataApiResponse +import com.frogobox.appkeyboard.data.remote.model.DataItemResponse +import com.frogobox.appkeyboard.repository.data.DataApiRepositoryImpl +import com.google.gson.Gson +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import retrofit2.Response +import java.net.ConnectException +import java.net.SocketTimeoutException +import java.net.UnknownHostException + +/** + * Modern Coroutine unit tests for DataApiRepository and remote data models. + * Strictly adheres to NO SUPPRESSION, ALWAYS MIGRATE policy using runTest and StandardTestDispatcher. + */ +class DataApiRepositoryTest { + + private class FakeDataApiService : DataApiService { + var responseToReturn: Response? = null + var exceptionToThrow: Throwable? = null + + override suspend fun getData(): Response { + exceptionToThrow?.let { throw it } + return responseToReturn + ?: Response.error(500, "Server Error".toResponseBody("text/plain".toMediaTypeOrNull())) + } + } + + private val sampleItems = listOf( + DataItemResponse( + id = 1, + rowIndex = 1, + title = "Tongsis Bluetooth", + caption = "Tongsis Bluetooth Wireless 3-in-1 Remote Shutter", + body = "Deskripsi produk tongsis tripod wireless bluetooth", + category = "Accessories", + fileSize = "3.26 MB", + isVideo = true, + uploaderName = "Celana Kulot Official", + uploadTimestamp = "2026-09-18 04:58:28 WIB", + driveLink = "https://drive.google.com/file/d/dummy123/view", + isActive = true + ), + DataItemResponse( + id = 2, + rowIndex = 2, + title = "Keyboard Mechanical RGB", + body = "Keyboard mechanical tactile switches RGB backlit", + category = "Hardware", + fileSize = "1.50 MB", + isVideo = false, + uploaderName = "Tech Store", + isActive = true + ) + ) + + private val sampleApiResponse = DataApiResponse( + code = 200, + status = "success", + message = "Data fetched successfully", + total = 2, + lastUpdated = "2026-09-19 08:00:00 WIB", + data = sampleItems + ) + + @Test + fun testFetchDataStream_success_emitsLoadingThenSuccess() = runTest { + val testDispatcher = StandardTestDispatcher(testScheduler) + val fakeApiService = FakeDataApiService().apply { + responseToReturn = Response.success(sampleApiResponse) + } + val repository = DataApiRepositoryImpl(fakeApiService, testDispatcher) + + val emissions = repository.fetchDataStream().toList() + + assertEquals(2, emissions.size) + assertTrue(emissions[0] is Resource.Loading) + assertTrue(emissions[1] is Resource.Success) + + val successResult = emissions[1] as Resource.Success + assertEquals(200, successResult.data.code) + assertEquals("success", successResult.data.status) + assertEquals(2, successResult.data.total) + assertEquals(2, successResult.data.data?.size) + assertEquals("Tongsis Bluetooth Wireless 3-in-1 Remote Shutter", successResult.data.data?.get(0)?.caption) + } + + @Test + fun testFetchDataStream_connectException_emitsLoadingThenError() = runTest { + val testDispatcher = StandardTestDispatcher(testScheduler) + val fakeApiService = FakeDataApiService().apply { + exceptionToThrow = ConnectException("Connection refused") + } + val repository = DataApiRepositoryImpl(fakeApiService, testDispatcher) + + val emissions = repository.fetchDataStream().toList() + + assertEquals(2, emissions.size) + assertTrue(emissions[0] is Resource.Loading) + assertTrue(emissions[1] is Resource.Error) + + val errorResult = emissions[1] as Resource.Error + assertTrue(errorResult.message.contains("Connection refused", ignoreCase = true)) + assertTrue(errorResult.cause is ConnectException) + } + + @Test + fun testFetchDataStream_socketTimeoutException_emitsLoadingThenError() = runTest { + val testDispatcher = StandardTestDispatcher(testScheduler) + val fakeApiService = FakeDataApiService().apply { + exceptionToThrow = SocketTimeoutException("Read timed out") + } + val repository = DataApiRepositoryImpl(fakeApiService, testDispatcher) + + val emissions = repository.fetchDataStream().toList() + + assertEquals(2, emissions.size) + assertTrue(emissions[0] is Resource.Loading) + assertTrue(emissions[1] is Resource.Error) + + val errorResult = emissions[1] as Resource.Error + assertTrue(errorResult.message.contains("timed out", ignoreCase = true)) + assertTrue(errorResult.cause is SocketTimeoutException) + } + + @Test + fun testFetchDataStream_unknownHostException_emitsLoadingThenError() = runTest { + val testDispatcher = StandardTestDispatcher(testScheduler) + val fakeApiService = FakeDataApiService().apply { + exceptionToThrow = UnknownHostException("192.168.100.6") + } + val repository = DataApiRepositoryImpl(fakeApiService, testDispatcher) + + val emissions = repository.fetchDataStream().toList() + + assertEquals(2, emissions.size) + assertTrue(emissions[0] is Resource.Loading) + assertTrue(emissions[1] is Resource.Error) + + val errorResult = emissions[1] as Resource.Error + assertTrue(errorResult.message.contains("Unable to resolve host", ignoreCase = true)) + } + + @Test + fun testFetchDataStream_http404_emitsLoadingThenError() = runTest { + val testDispatcher = StandardTestDispatcher(testScheduler) + val fakeApiService = FakeDataApiService().apply { + responseToReturn = Response.error( + 404, + "Endpoint not found".toResponseBody("text/plain".toMediaTypeOrNull()) + ) + } + val repository = DataApiRepositoryImpl(fakeApiService, testDispatcher) + + val emissions = repository.fetchDataStream().toList() + + assertEquals(2, emissions.size) + assertTrue(emissions[0] is Resource.Loading) + assertTrue(emissions[1] is Resource.Error) + + val errorResult = emissions[1] as Resource.Error + assertEquals(404, errorResult.code) + assertTrue(errorResult.message.contains("404")) + } + + @Test + fun testFetchData_directSuspend_success() = runTest { + val testDispatcher = StandardTestDispatcher(testScheduler) + val fakeApiService = FakeDataApiService().apply { + responseToReturn = Response.success(sampleApiResponse) + } + val repository = DataApiRepositoryImpl(fakeApiService, testDispatcher) + + val result = repository.fetchData() + + assertTrue(result is Resource.Success) + val successResult = result as Resource.Success + assertEquals(2, successResult.data.total) + } + + @Test + fun testGsonSerializationDeserialization_matchesContract() { + val gson = Gson() + val json = """ + { + "code": 200, + "status": "success", + "message": "OK", + "total": 1, + "lastUpdated": "2026-09-19 12:00:00 WIB", + "data": [ + { + "id": 10, + "rowIndex": 10, + "title": "Tripod Mini", + "caption": "Tripod Mini Flexible Stand", + "body": "Flexible spider tripod for mobile photography", + "category": "Camera", + "isVideo": true, + "fileSize": "2.40 MB", + "uploaderName": "PhotoStudio", + "uploadTimestamp": "2026-09-18 10:00:00 WIB", + "driveLink": "https://drive.google.com/file/d/test", + "thumbnailUrl": "https://example.com/thumb.jpg", + "isActive": true + } + ] + } + """.trimIndent() + + val parsed = gson.fromJson(json, DataApiResponse::class.java) + + assertNotNull(parsed) + assertEquals(200, parsed.code) + assertEquals("success", parsed.status) + assertEquals(1, parsed.total) + assertNotNull(parsed.data) + assertEquals(1, parsed.data?.size) + + val item = parsed.data?.first()!! + assertEquals(10, item.displayIndex) + assertEquals("Tripod Mini Flexible Stand", item.displayTitle) + assertEquals("PhotoStudio", item.displaySubtitle) + assertEquals("Flexible spider tripod for mobile photography", item.displayBody) + assertEquals("2026-09-18 10:00:00 WIB", item.displayTimestamp) + assertEquals(true, item.isVideo) + assertEquals("2.40 MB", item.fileSize) + assertEquals("https://drive.google.com/file/d/test", item.driveLink) + assertEquals("https://example.com/thumb.jpg", item.thumbnailUrl) + } + + @Test + fun testDataItemResponse_fallbackDisplayProperties() { + // Item with minimal fields + val minimalItem = DataItemResponse(id = 5, title = "Simple Title") + assertEquals(5, minimalItem.displayIndex) + assertEquals("Simple Title", minimalItem.displayTitle) + assertNull(minimalItem.displaySubtitle) + assertNull(minimalItem.displayBody) + assertNull(minimalItem.displayTimestamp) + + // Item with only rowIndex and uploaderName + val uploaderOnlyItem = DataItemResponse(rowIndex = 12, uploaderName = "Admin") + assertEquals(12, uploaderOnlyItem.displayIndex) + assertEquals("Admin", uploaderOnlyItem.displayTitle) + assertEquals("Admin", uploaderOnlyItem.displaySubtitle) + + // Completely empty item + val emptyItem = DataItemResponse() + assertEquals(0, emptyItem.displayIndex) + assertEquals("Item #0", emptyItem.displayTitle) + assertNull(emptyItem.displaySubtitle) + assertNull(emptyItem.displayBody) + } +} diff --git a/app/src/test/java/com/frogobox/appkeyboard/ui/test/DataApiViewModelTest.kt b/app/src/test/java/com/frogobox/appkeyboard/ui/test/DataApiViewModelTest.kt new file mode 100644 index 0000000..e6b09ec --- /dev/null +++ b/app/src/test/java/com/frogobox/appkeyboard/ui/test/DataApiViewModelTest.kt @@ -0,0 +1,209 @@ +package com.frogobox.appkeyboard.ui.test + +import com.frogobox.appkeyboard.common.callback.DataResponseCallback +import com.frogobox.appkeyboard.common.callback.StateResponseCallback +import com.frogobox.appkeyboard.common.core.Resource +import com.frogobox.appkeyboard.data.remote.model.DataApiResponse +import com.frogobox.appkeyboard.data.remote.model.DataItemResponse +import com.frogobox.appkeyboard.model.AutoTextEntity +import com.frogobox.appkeyboard.model.AutoTextLabelType +import com.frogobox.appkeyboard.repository.autotext.AutoTextRepository +import com.frogobox.appkeyboard.repository.data.DataApiRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +/** + * Modern Coroutine unit tests for TestViewModel UI state transitions regarding remote data API. + * Adheres strictly to NO SUPPRESSION, ALWAYS MIGRATE policy using runTest and TestDispatchers. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class DataApiViewModelTest { + + private val testDispatcher = StandardTestDispatcher() + + private class FakeAutoTextRepository : AutoTextRepository { + override fun getAutoText(): Flow> = flowOf(emptyList()) + override fun getAutoTextByLabel(label: AutoTextLabelType): Flow> = flowOf(emptyList()) + override fun getAutoTextByTitle(title: String): Flow> = flowOf(emptyList()) + override fun getAutoTextByBody(body: String): Flow> = flowOf(emptyList()) + override fun getAutoTextByTitleOrBody(keyword: String): Flow> = flowOf(emptyList()) + override suspend fun insertAutoText(autoText: AutoTextEntity) {} + override suspend fun insertAutoText(autoTexts: List) {} + override suspend fun updateAutoText(autoText: AutoTextEntity) {} + override suspend fun deleteAutoText(autoText: AutoTextEntity) {} + override suspend fun deleteAutoText(idList: List) {} + override suspend fun nukeAutoText() {} + + override fun getAutoText(callback: DataResponseCallback>) {} + override fun getAutoTextByLabel(label: AutoTextLabelType, callback: DataResponseCallback>) {} + override fun getAutoTextByTitle(title: String, callback: DataResponseCallback>) {} + override fun getAutoTextByBody(body: String, callback: DataResponseCallback>) {} + override fun getAutoTextByTitleOrBody(keyword: String, callback: DataResponseCallback>) {} + override fun insertAutoText(autoText: AutoTextEntity, callback: StateResponseCallback) {} + override fun insertAutoText(autoTexts: List, callback: StateResponseCallback) {} + override fun updateAutoText(autoText: AutoTextEntity, callback: StateResponseCallback) {} + override fun deleteAutoText(autoText: AutoTextEntity, callback: StateResponseCallback) {} + override fun deleteAutoText(idList: List, callback: StateResponseCallback) {} + override fun nukeAutoText(callback: StateResponseCallback) {} + } + + private class FakeDataApiRepository : DataApiRepository { + var flowToReturn: Flow> = emptyFlow() + var directResultToReturn: Resource = Resource.Success(DataApiResponse(code = 200)) + + override fun fetchDataStream(): Flow> = flowToReturn + override suspend fun fetchData(): Resource = directResultToReturn + } + + @Before + fun setUp() { + Dispatchers.setMain(testDispatcher) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun testInitialState_isIdle() = runTest(testDispatcher) { + val fakeAutoTextRepo = FakeAutoTextRepository() + val fakeDataApiRepo = FakeDataApiRepository() + val viewModel = TestViewModel(fakeAutoTextRepo, fakeDataApiRepo) + + advanceUntilIdle() + + assertEquals(DataApiUiState.Idle, viewModel.remoteApiUiState.value) + } + + @Test + fun testFetchRemoteData_success_transitionsThroughLoadingToSuccess() = runTest(testDispatcher) { + val fakeAutoTextRepo = FakeAutoTextRepository() + val fakeDataApiRepo = FakeDataApiRepository() + + val sampleItems = listOf( + DataItemResponse(id = 1, title = "Wireless Mouse", body = "2.4GHz wireless mouse") + ) + val successResponse = DataApiResponse( + code = 200, + status = "success", + total = 1, + lastUpdated = "2026-09-19 15:00:00 WIB", + data = sampleItems + ) + + fakeDataApiRepo.flowToReturn = flow { + emit(Resource.Loading) + emit(Resource.Success(successResponse)) + } + + val viewModel = TestViewModel(fakeAutoTextRepo, fakeDataApiRepo) + val states = mutableListOf() + val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) { + viewModel.remoteApiUiState.toList(states) + } + + viewModel.fetchRemoteData() + advanceUntilIdle() + + assertEquals(3, states.size) + assertTrue(states[0] is DataApiUiState.Idle) + assertTrue(states[1] is DataApiUiState.Loading) + assertTrue(states[2] is DataApiUiState.Success) + + val successState = states[2] as DataApiUiState.Success + assertEquals(1, successState.total) + assertEquals("2026-09-19 15:00:00 WIB", successState.lastUpdatedWib) + assertEquals(1, successState.items.size) + assertEquals("Wireless Mouse", successState.items[0].title) + + collectJob.cancel() + } + + @Test + fun testFetchRemoteData_error_transitionsThroughLoadingToError() = runTest(testDispatcher) { + val fakeAutoTextRepo = FakeAutoTextRepository() + val fakeDataApiRepo = FakeDataApiRepository() + val errorMsg = "Connection refused. Please ensure the local server is running at http://192.168.100.6:3000." + + fakeDataApiRepo.flowToReturn = flow { + emit(Resource.Loading) + emit(Resource.Error(message = errorMsg)) + } + + val viewModel = TestViewModel(fakeAutoTextRepo, fakeDataApiRepo) + val states = mutableListOf() + val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) { + viewModel.remoteApiUiState.toList(states) + } + + viewModel.fetchRemoteData() + advanceUntilIdle() + + assertEquals(3, states.size) + assertTrue(states[0] is DataApiUiState.Idle) + assertTrue(states[1] is DataApiUiState.Loading) + assertTrue(states[2] is DataApiUiState.Error) + + val errorState = states[2] as DataApiUiState.Error + assertEquals(errorMsg, errorState.message) + + collectJob.cancel() + } + + @Test + fun testResetRemoteData_revertsStateToIdle() = runTest(testDispatcher) { + val fakeAutoTextRepo = FakeAutoTextRepository() + val fakeDataApiRepo = FakeDataApiRepository() + + fakeDataApiRepo.flowToReturn = flow { + emit(Resource.Loading) + } + + val viewModel = TestViewModel(fakeAutoTextRepo, fakeDataApiRepo) + advanceUntilIdle() + + viewModel.fetchRemoteData() + advanceUntilIdle() + assertTrue(viewModel.remoteApiUiState.value is DataApiUiState.Loading) + + viewModel.resetRemoteData() + assertEquals(DataApiUiState.Idle, viewModel.remoteApiUiState.value) + } + + @Test + fun testInsertText_handlesEmptyAndExistingContent() = runTest(testDispatcher) { + val fakeAutoTextRepo = FakeAutoTextRepository() + val fakeDataApiRepo = FakeDataApiRepository() + val viewModel = TestViewModel(fakeAutoTextRepo, fakeDataApiRepo) + advanceUntilIdle() + + assertEquals("", viewModel.sandboxText.value) + + viewModel.onInsertText("First line from API") + assertEquals("First line from API", viewModel.sandboxText.value) + + viewModel.onInsertText("Second line from API") + assertEquals("First line from API\nSecond line from API", viewModel.sandboxText.value) + + viewModel.onClearText() + assertEquals("", viewModel.sandboxText.value) + } +} From 0ca75f38f8ecaa2ef1137e7efbc9080e5ae49e38 Mon Sep 17 00:00:00 2001 From: amirisback Date: Sun, 20 Sep 2026 07:06:04 +0700 Subject: [PATCH 3/8] feat: implement DataApiService and DataApiRepository for remote data fetching --- app/build.gradle.kts | 1 + .../appkeyboard/services/ImeLifecycleOwner.kt | 61 +++++ .../appkeyboard/services/KeyboardIME.kt | 66 ++--- .../ui/keyboard/autotext/AutoTextKeyboard.kt | 153 ++++------- .../autotext/AutoTextKeyboardScreen.kt | 136 +++++++++ .../ui/keyboard/common/AsyncGlideImage.kt | 91 ++++++ .../keyboard/common/KeyboardFeatureToolbar.kt | 105 +++++++ .../ui/keyboard/form/FormKeyboard.kt | 82 +++--- .../ui/keyboard/form/FormKeyboardScreen.kt | 186 +++++++++++++ .../ui/keyboard/movie/MovieKeyboard.kt | 176 +++++------- .../ui/keyboard/movie/MovieKeyboardScreen.kt | 171 ++++++++++++ .../ui/keyboard/news/NewsKeyboard.kt | 179 ++++-------- .../ui/keyboard/news/NewsKeyboardScreen.kt | 161 +++++++++++ .../templatetext/TemplateTextKeyboard.kt | 249 ++++------------- .../TemplateTextKeyboardScreen.kt | 162 +++++++++++ .../templatetext/TemplateTextUtils.kt | 1 - .../ui/keyboard/webview/WebiewKeyboard.kt | 121 +++----- .../keyboard/webview/WebviewKeyboardScreen.kt | 259 ++++++++++++++++++ .../ui/language/KeyboardLanguageActivity.kt | 3 +- .../ui/language/KeyboardLanguageViewModel.kt | 1 - .../appkeyboard/ui/sound/SoundActivity.kt | 3 +- .../appkeyboard/ui/sound/SoundScreen.kt | 2 - .../appkeyboard/ui/test/TestScreen.kt | 7 - .../appkeyboard/ui/theme/ThemeScreen.kt | 7 - .../ui/theme/compose/FrogoEmptyView.kt | 4 +- app/src/main/res/drawable/bg_border.xml | 11 - app/src/main/res/drawable/bg_card_16dp.xml | 24 -- app/src/main/res/drawable/bg_card_8dp.xml | 24 -- .../res/drawable/bg_feature_action_pill.xml | 6 - .../res/drawable/bg_feature_card_12dp.xml | 9 - .../res/drawable/bg_feature_chip_active.xml | 9 - .../res/drawable/bg_feature_chip_inactive.xml | 9 - .../res/drawable/bg_feature_rating_badge.xml | 11 - .../drawable/bg_feature_toolbar_button.xml | 14 - app/src/main/res/drawable/ic_check.xml | 18 -- .../main/res/drawable/ic_feature_insert.xml | 9 - .../main/res/drawable/ic_feature_refresh.xml | 9 - .../main/res/drawable/ic_feature_search.xml | 9 - app/src/main/res/drawable/ic_feature_star.xml | 9 - app/src/main/res/layout/activity_autotext.xml | 44 --- .../res/layout/activity_autotext_detail.xml | 73 ----- .../res/layout/activity_autotext_editor.xml | 53 ---- .../res/layout/activity_keyboard_language.xml | 12 - app/src/main/res/layout/activity_main.xml | 199 -------------- app/src/main/res/layout/activity_theme.xml | 11 - app/src/main/res/layout/activity_toggle.xml | 12 - app/src/main/res/layout/item_autotext.xml | 29 -- .../res/layout/item_keyboard_autotext.xml | 98 ------- .../main/res/layout/item_keyboard_movie.xml | 131 --------- .../res/layout/item_keyboard_multi_form.xml | 18 -- .../main/res/layout/item_keyboard_news.xml | 111 -------- .../item_keyboard_template_category.xml | 33 --- .../layout/item_keyboard_template_text.xml | 72 ----- .../res/layout/item_language_keyboard.xml | 31 --- app/src/main/res/layout/item_spinner.xml | 18 -- app/src/main/res/layout/item_theme.xml | 85 ------ app/src/main/res/layout/item_toggle.xml | 39 --- app/src/main/res/layout/keyboard_autotext.xml | 171 ------------ app/src/main/res/layout/keyboard_form.xml | 213 -------------- app/src/main/res/layout/keyboard_grid.xml | 99 ------- app/src/main/res/layout/keyboard_list.xml | 99 ------- .../main/res/layout/keyboard_multi_form.xml | 82 ------ .../res/layout/keyboard_template_text.xml | 101 ------- app/src/main/res/layout/keyboard_webview.xml | 149 ---------- app/src/main/res/layout/view_empty.xml | 33 --- app/src/main/res/values-night/color.xml | 12 - app/src/main/res/values/color.xml | 12 - ...echanicalSoundAndProgrammerKeyboardTest.kt | 1 - .../com/frogobox/appkeyboard/ThemeTest.kt | 1 - .../appkeyboard/WordSuggestionEngineTest.kt | 1 - .../libkeyboard/ui/main/MainKeyboard.kt | 1 - .../libkeyboard/BaseKeyboardIMETest.kt | 5 - gradle/libs.versions.toml | 1 + 73 files changed, 1662 insertions(+), 2946 deletions(-) create mode 100644 app/src/main/java/com/frogobox/appkeyboard/services/ImeLifecycleOwner.kt create mode 100644 app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/autotext/AutoTextKeyboardScreen.kt create mode 100644 app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/common/AsyncGlideImage.kt create mode 100644 app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/common/KeyboardFeatureToolbar.kt create mode 100644 app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboardScreen.kt create mode 100644 app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/movie/MovieKeyboardScreen.kt create mode 100644 app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/news/NewsKeyboardScreen.kt create mode 100644 app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboardScreen.kt create mode 100644 app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/webview/WebviewKeyboardScreen.kt delete mode 100644 app/src/main/res/drawable/bg_border.xml delete mode 100644 app/src/main/res/drawable/bg_card_16dp.xml delete mode 100644 app/src/main/res/drawable/bg_card_8dp.xml delete mode 100644 app/src/main/res/drawable/bg_feature_action_pill.xml delete mode 100644 app/src/main/res/drawable/bg_feature_card_12dp.xml delete mode 100644 app/src/main/res/drawable/bg_feature_chip_active.xml delete mode 100644 app/src/main/res/drawable/bg_feature_chip_inactive.xml delete mode 100644 app/src/main/res/drawable/bg_feature_rating_badge.xml delete mode 100644 app/src/main/res/drawable/bg_feature_toolbar_button.xml delete mode 100644 app/src/main/res/drawable/ic_check.xml delete mode 100644 app/src/main/res/drawable/ic_feature_insert.xml delete mode 100644 app/src/main/res/drawable/ic_feature_refresh.xml delete mode 100644 app/src/main/res/drawable/ic_feature_search.xml delete mode 100644 app/src/main/res/drawable/ic_feature_star.xml delete mode 100644 app/src/main/res/layout/activity_autotext.xml delete mode 100644 app/src/main/res/layout/activity_autotext_detail.xml delete mode 100644 app/src/main/res/layout/activity_autotext_editor.xml delete mode 100644 app/src/main/res/layout/activity_keyboard_language.xml delete mode 100644 app/src/main/res/layout/activity_main.xml delete mode 100644 app/src/main/res/layout/activity_theme.xml delete mode 100644 app/src/main/res/layout/activity_toggle.xml delete mode 100644 app/src/main/res/layout/item_autotext.xml delete mode 100644 app/src/main/res/layout/item_keyboard_autotext.xml delete mode 100644 app/src/main/res/layout/item_keyboard_movie.xml delete mode 100644 app/src/main/res/layout/item_keyboard_multi_form.xml delete mode 100644 app/src/main/res/layout/item_keyboard_news.xml delete mode 100644 app/src/main/res/layout/item_keyboard_template_category.xml delete mode 100644 app/src/main/res/layout/item_keyboard_template_text.xml delete mode 100644 app/src/main/res/layout/item_language_keyboard.xml delete mode 100644 app/src/main/res/layout/item_spinner.xml delete mode 100644 app/src/main/res/layout/item_theme.xml delete mode 100644 app/src/main/res/layout/item_toggle.xml delete mode 100644 app/src/main/res/layout/keyboard_autotext.xml delete mode 100644 app/src/main/res/layout/keyboard_form.xml delete mode 100644 app/src/main/res/layout/keyboard_grid.xml delete mode 100644 app/src/main/res/layout/keyboard_list.xml delete mode 100644 app/src/main/res/layout/keyboard_multi_form.xml delete mode 100644 app/src/main/res/layout/keyboard_template_text.xml delete mode 100644 app/src/main/res/layout/keyboard_webview.xml delete mode 100644 app/src/main/res/layout/view_empty.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cd13613..844d883 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -125,6 +125,7 @@ dependencies { implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material.icons.extended) implementation(libs.androidx.activity.compose) + implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(project(DependencyGradle.MODULE_LIB_FROGO_KEYBOARD)) diff --git a/app/src/main/java/com/frogobox/appkeyboard/services/ImeLifecycleOwner.kt b/app/src/main/java/com/frogobox/appkeyboard/services/ImeLifecycleOwner.kt new file mode 100644 index 0000000..54952d4 --- /dev/null +++ b/app/src/main/java/com/frogobox/appkeyboard/services/ImeLifecycleOwner.kt @@ -0,0 +1,61 @@ +package com.frogobox.appkeyboard.services + +import android.os.Bundle +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.ViewModelStoreOwner +import androidx.savedstate.SavedStateRegistry +import androidx.savedstate.SavedStateRegistryController +import androidx.savedstate.SavedStateRegistryOwner + +/** + * Custom [LifecycleOwner], [ViewModelStoreOwner], and [SavedStateRegistryOwner] implementation + * for [android.inputmethodservice.InputMethodService]. + * + * Enables Jetpack Compose [androidx.compose.ui.platform.ComposeView] to be safely hosted within + * the keyboard service window without missing lifecycle owners or leaking memory. + */ +class ImeLifecycleOwner : LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner { + + private val lifecycleRegistry = LifecycleRegistry(this) + private val store = ViewModelStore() + private val savedStateRegistryController = SavedStateRegistryController.create(this) + + override val lifecycle: Lifecycle + get() = lifecycleRegistry + + override val viewModelStore: ViewModelStore + get() = store + + override val savedStateRegistry: SavedStateRegistry + get() = savedStateRegistryController.savedStateRegistry + + fun onCreate() { + savedStateRegistryController.performRestore(Bundle()) + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + } + + fun onStart() { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) + } + + fun onResume() { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) + } + + fun onPause() { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) + } + + fun onStop() { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) + } + + fun onDestroy() { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + store.clear() + } + +} diff --git a/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt b/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt index 0b5070c..96858f0 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt @@ -2,9 +2,7 @@ package com.frogobox.appkeyboard.services import android.content.Intent import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection import android.view.inputmethod.InputMethodManager import android.widget.EditText @@ -28,6 +26,9 @@ import com.frogobox.sdk.ext.gone import com.frogobox.sdk.ext.invisible import com.frogobox.appkeyboard.suggestion.WordSuggestionEngine import com.frogobox.sdk.ext.visible +import androidx.lifecycle.setViewTreeLifecycleOwner +import androidx.lifecycle.setViewTreeViewModelStoreOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -35,6 +36,8 @@ import javax.inject.Inject @AndroidEntryPoint class KeyboardIME : BaseKeyboardIME() { + private val imeLifecycleOwner = ImeLifecycleOwner() + @Inject lateinit var pref: PreferenceDelegates @@ -114,13 +117,6 @@ class KeyboardIME : BaseKeyboardIME() { KeyboardFeatureType.FORM -> { this@KeyboardIME.binding?.keyboardHeader?.gone() this@KeyboardIME.binding?.keyboardForm?.visible() - this@KeyboardIME.binding?.keyboardForm?.binding?.etText?.showKeyboardExt() - this@KeyboardIME.binding?.keyboardForm?.binding?.etText2?.showKeyboardExt() - this@KeyboardIME.binding?.keyboardForm?.binding?.etText3?.showKeyboardExt() - - this@KeyboardIME.binding?.keyboardForm?.setOnClickListener { - hideOnlyKeyboard() - } } KeyboardFeatureType.AUTO_TEXT -> { @@ -210,11 +206,24 @@ class KeyboardIME : BaseKeyboardIME() { } } + override fun onCreate() { + super.onCreate() + imeLifecycleOwner.onCreate() + } + override fun onWindowShown() { super.onWindowShown() + imeLifecycleOwner.onStart() + imeLifecycleOwner.onResume() applySoundAndHapticSettings() } + override fun onWindowHidden() { + super.onWindowHidden() + imeLifecycleOwner.onPause() + imeLifecycleOwner.onStop() + } + override fun initialSetupKeyboard() { applySoundAndHapticSettings() binding?.keyboardMain?.setKeyboard(keyboard!!) @@ -225,8 +234,16 @@ class KeyboardIME : BaseKeyboardIME() { binding?.apply { keyboardMain.mOnKeyboardActionListener = this@KeyboardIME keyboardEmoji.mOnKeyboardActionListener = this@KeyboardIME - } + root.setViewTreeLifecycleOwner(imeLifecycleOwner) + root.setViewTreeViewModelStoreOwner(imeLifecycleOwner) + root.setViewTreeSavedStateRegistryOwner(imeLifecycleOwner) + } + window?.window?.decorView?.let { decor -> + decor.setViewTreeLifecycleOwner(imeLifecycleOwner) + decor.setViewTreeViewModelStoreOwner(imeLifecycleOwner) + decor.setViewTreeSavedStateRegistryOwner(imeLifecycleOwner) + } } override fun invalidateKeyboard() { @@ -294,27 +311,27 @@ class KeyboardIME : BaseKeyboardIME() { override fun initBackToMainKeyboard() { binding?.apply { - keyboardAutotext.binding.toolbarBack.setOnClickListener { + keyboardAutotext.setOnBackClickListener { keyboardAutotext.gone() showMainKeyboard() } - keyboardNews.binding.toolbarBack.setOnClickListener { + keyboardNews.setOnBackClickListener { keyboardNews.gone() showMainKeyboard() } - keyboardMoview.binding.toolbarBack.setOnClickListener { + keyboardMoview.setOnBackClickListener { keyboardMoview.gone() showMainKeyboard() } - keyboardWebview.binding.toolbarBack.setOnClickListener { + keyboardWebview.setOnBackClickListener { keyboardWebview.gone() showMainKeyboard() } - keyboardForm.binding.toolbarBack.setOnClickListener { + keyboardForm.setOnBackClickListener { keyboardForm.gone() showMainKeyboard() } @@ -325,7 +342,7 @@ class KeyboardIME : BaseKeyboardIME() { showMainKeyboard() } - keyboardTemplateText.binding.toolbarBack.setOnClickListener { + keyboardTemplateText.setOnBackClickListener { keyboardTemplateText.gone() showMainKeyboard() } @@ -358,22 +375,6 @@ class KeyboardIME : BaseKeyboardIME() { private fun getActiveInputConnection(): InputConnection? { - val formView = binding?.keyboardForm - if (formView?.visibility == View.VISIBLE) { - val et1 = formView.binding.etText - val et2 = formView.binding.etText2 - val et3 = formView.binding.etText3 - - if (et1.isFocused) { - return et1.onCreateInputConnection(EditorInfo()) - } else if (et2.isFocused) { - return et2.onCreateInputConnection(EditorInfo()) - } else if (et3.isFocused) { - return et3.onCreateInputConnection(EditorInfo()) - } - } else if (binding?.keyboardWebview?.visibility == View.VISIBLE) { - return binding?.keyboardWebview?.binding?.webview?.onCreateInputConnection(EditorInfo()) - } return currentInputConnection } @@ -525,6 +526,7 @@ class KeyboardIME : BaseKeyboardIME() { override fun onDestroy() { super.onDestroy() + imeLifecycleOwner.onDestroy() MechanicalSoundManager.getInstance(this).release() } diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/autotext/AutoTextKeyboard.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/autotext/AutoTextKeyboard.kt index 38fe6ae..2926287 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/autotext/AutoTextKeyboard.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/autotext/AutoTextKeyboard.kt @@ -3,125 +3,72 @@ package com.frogobox.appkeyboard.ui.keyboard.autotext import android.content.Context import android.content.Intent import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.LinearLayout -import com.frogobox.appkeyboard.databinding.ItemKeyboardAutotextBinding -import com.frogobox.appkeyboard.databinding.KeyboardAutotextBinding +import android.view.inputmethod.InputConnection +import android.widget.FrameLayout +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import com.frogobox.appkeyboard.model.AutoTextEntity import com.frogobox.appkeyboard.ui.autotext.AutoTextActivity -import com.frogobox.libkeyboard.common.core.BaseKeyboard -import com.frogobox.recycler.core.FrogoRecyclerNotifyListener -import com.frogobox.recycler.core.IFrogoBindingAdapter -import com.frogobox.recycler.ext.injectorBinding +import com.frogobox.appkeyboard.ui.theme.compose.FrogoKeyboardTheme +import kotlinx.coroutines.flow.MutableStateFlow /** - * Created by Faisal Amir on 11/03/23 - * https://github.com/amirisback + * Modern Jetpack Compose-based AutoText Keyboard panel. */ - -class AutoTextKeyboard( +class AutoTextKeyboard @JvmOverloads constructor( context: Context, - attrs: AttributeSet?, -) : BaseKeyboard(context, attrs) { + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { - override fun setupViewBinding(inflater: LayoutInflater, parent: LinearLayout): KeyboardAutotextBinding { - return KeyboardAutotextBinding.inflate(LayoutInflater.from(context), this, true) - } + private val viewModel by lazy { AutoTextKeyboardViewModel(context) } + private val autoTextState = MutableStateFlow>(emptyList()) - override fun initUI() { - super.initUI() - binding.apply { - tvToolbarTitle.text = "Auto Text" - val openManage = { - val intent = Intent(context, AutoTextActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - context.startActivity(intent) + var currentInputConnection: InputConnection? = null + var onBackClick: (() -> Unit)? = null + + private val composeView = ComposeView(context).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool) + setContent { + FrogoKeyboardTheme { + val list by autoTextState.collectAsState() + AutoTextKeyboardScreen( + autoTextList = list, + onCommitText = { text -> + currentInputConnection?.commitText(text, 1) + }, + onBackClick = { + onBackClick?.invoke() + }, + onManageClick = { + val intent = Intent(context, AutoTextActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + context.startActivity(intent) + } + ) } - toolbarManage.setOnClickListener { openManage() } - btnAddAutotext.setOnClickListener { openManage() } } } - override fun initData() { - super.initData() - val viewModel = AutoTextKeyboardViewModel(context) - viewModel.getAutoText { - setupRv(it) - } + init { + addView(composeView) + initData() } - private fun setupRv(data: List) { - binding.apply { - if (data.isEmpty()) { - llEmptyState.visibility = View.VISIBLE - rvKeyboardMain.visibility = View.GONE - } else { - llEmptyState.visibility = View.GONE - rvKeyboardMain.visibility = View.VISIBLE - } - - val adapterCallback = object : IFrogoBindingAdapter { - override fun onItemClicked( - binding: ItemKeyboardAutotextBinding, - data: AutoTextEntity, - position: Int, - notifyListener: FrogoRecyclerNotifyListener, - ) { - val output = data.body - currentInputConnection?.commitText(output, 1) - } - - override fun onItemLongClicked( - binding: ItemKeyboardAutotextBinding, - data: AutoTextEntity, - position: Int, - notifyListener: FrogoRecyclerNotifyListener, - ) { - } - - override fun areContentsTheSame( - oldItem: AutoTextEntity, - newItem: AutoTextEntity - ): Boolean { - return oldItem == newItem - } - - override fun areItemsTheSame( - oldItem: AutoTextEntity, - newItem: AutoTextEntity - ): Boolean { - return oldItem.id == newItem.id - } - - override fun setViewBinding(parent: ViewGroup): ItemKeyboardAutotextBinding { - return ItemKeyboardAutotextBinding.inflate( - LayoutInflater.from(context), - parent, - false - ) - } + fun setInputConnection(inputConnection: InputConnection?) { + currentInputConnection = inputConnection + } - override fun setupInitComponent( - binding: ItemKeyboardAutotextBinding, - data: AutoTextEntity, - position: Int, - notifyListener: FrogoRecyclerNotifyListener, - ) { - binding.apply { - tvItemKeyboardMain.text = data.title - tvItemKeyboardBody.text = data.body - } - } - } + fun setOnBackClickListener(listener: () -> Unit) { + onBackClick = listener + } - rvKeyboardMain.injectorBinding() - .addData(data) - .createLayoutLinearVertical(false) - .addCallback(adapterCallback) - .build() + fun initData() { + viewModel.getAutoText { items -> + autoTextState.value = items } } diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/autotext/AutoTextKeyboardScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/autotext/AutoTextKeyboardScreen.kt new file mode 100644 index 0000000..cb32df5 --- /dev/null +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/autotext/AutoTextKeyboardScreen.kt @@ -0,0 +1,136 @@ +package com.frogobox.appkeyboard.ui.keyboard.autotext + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.frogobox.appkeyboard.model.AutoTextEntity +import com.frogobox.appkeyboard.ui.keyboard.common.KeyboardFeatureToolbar + +@Composable +fun AutoTextKeyboardScreen( + autoTextList: List, + onCommitText: (String) -> Unit, + onBackClick: () -> Unit, + onManageClick: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + ) { + KeyboardFeatureToolbar( + title = "Auto Text", + subtitle = "Tap to insert shortcut into chat", + onBackClick = onBackClick, + action = { + Box( + modifier = Modifier + .size(34.dp) + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + .clickable { onManageClick() } + .padding(6.dp), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = "Manage Auto Text", + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.onSurface + ) + } + } + ) + + if (autoTextList.isEmpty()) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + com.frogobox.appkeyboard.ui.theme.compose.FrogoEmptyView( + title = "No Auto Text Found", + subtitle = "Create reusable snippets to save time while typing" + ) + } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 10.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + items( + items = autoTextList, + key = { it.id } + ) { item -> + Card( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .clickable { onCommitText(item.body) }, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f) + ), + shape = RoundedCornerShape(10.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp) + ) { + Text( + text = item.title, + style = MaterialTheme.typography.titleSmall.copy( + fontSize = 13.5.sp, + fontWeight = FontWeight.Bold + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = item.body, + style = MaterialTheme.typography.bodySmall.copy( + fontSize = 11.5.sp + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/common/AsyncGlideImage.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/common/AsyncGlideImage.kt new file mode 100644 index 0000000..fde8485 --- /dev/null +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/common/AsyncGlideImage.kt @@ -0,0 +1,91 @@ +package com.frogobox.appkeyboard.ui.keyboard.common + +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.bumptech.glide.Glide +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition + +@Composable +fun AsyncGlideImage( + url: String?, + contentDescription: String?, + modifier: Modifier = Modifier, + contentScale: ContentScale = ContentScale.Crop +) { + val context = LocalContext.current + var bitmap by remember(url) { mutableStateOf(null) } + var isLoading by remember(url) { mutableStateOf(!url.isNullOrBlank()) } + + DisposableEffect(url) { + if (url.isNullOrBlank()) { + isLoading = false + return@DisposableEffect onDispose {} + } + + val target = object : CustomTarget() { + override fun onResourceReady(resource: Bitmap, transition: Transition?) { + bitmap = resource + isLoading = false + } + + override fun onLoadCleared(placeholder: Drawable?) { + bitmap = null + isLoading = false + } + + override fun onLoadFailed(errorDrawable: Drawable?) { + isLoading = false + } + } + + Glide.with(context) + .asBitmap() + .load(url) + .into(target) + + onDispose { + Glide.with(context).clear(target) + } + } + + Box( + modifier = modifier.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)), + contentAlignment = Alignment.Center + ) { + val currentBitmap = bitmap + if (currentBitmap != null) { + Image( + bitmap = currentBitmap.asImageBitmap(), + contentDescription = contentDescription, + modifier = Modifier.fillMaxSize(), + contentScale = contentScale + ) + } else if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.primary + ) + } + } +} diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/common/KeyboardFeatureToolbar.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/common/KeyboardFeatureToolbar.kt new file mode 100644 index 0000000..075e00b --- /dev/null +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/common/KeyboardFeatureToolbar.kt @@ -0,0 +1,105 @@ +package com.frogobox.appkeyboard.ui.keyboard.common + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun KeyboardFeatureToolbar( + title: String, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + subtitle: String? = null, + action: (@Composable () -> Unit)? = null +) { + Column(modifier = modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(50.dp) + .background(MaterialTheme.colorScheme.surface) + .padding(horizontal = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Tactile Back Button + Box( + modifier = Modifier + .size(34.dp) + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + .clickable { onBackClick() } + .padding(6.dp), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.onSurface + ) + } + + Spacer(modifier = Modifier.width(10.dp)) + + // Title & Subtitle block + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium.copy( + fontSize = 15.sp, + fontWeight = FontWeight.Bold + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + if (!subtitle.isNullOrBlank()) { + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall.copy( + fontSize = 10.5.sp + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + + if (action != null) { + Spacer(modifier = Modifier.width(8.dp)) + action() + } + } + + HorizontalDivider( + thickness = 0.8.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f) + ) + } +} diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboard.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboard.kt index 49924dd..c112922 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboard.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboard.kt @@ -2,60 +2,50 @@ package com.frogobox.appkeyboard.ui.keyboard.form import android.content.Context import android.util.AttributeSet -import android.view.LayoutInflater -import android.widget.LinearLayout -import com.frogobox.appkeyboard.databinding.KeyboardFormBinding -import com.frogobox.appkeyboard.ui.keyboard.movie.MovieKeyboard -import com.frogobox.appkeyboard.util.KeyboardNavigationHelper -import com.frogobox.libkeyboard.common.core.BaseKeyboard +import android.view.inputmethod.InputConnection +import android.widget.FrameLayout +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import com.frogobox.appkeyboard.ui.theme.compose.FrogoKeyboardTheme /** - * Created by Faisal Amir on 07/11/22 - * ----------------------------------------- - * E-mail : faisalamircs@gmail.com - * Github : github.com/amirisback - * ----------------------------------------- - * Copyright (C) Frogobox ID / amirisback - * All rights reserved + * Modern Jetpack Compose-based Quick Form Keyboard panel. */ - -class FormKeyboard( +class FormKeyboard @JvmOverloads constructor( context: Context, - attrs: AttributeSet?, -) : BaseKeyboard(context, attrs) { - - private val destinationKeyboard by lazy { MovieKeyboard(context, attrs) } - - override fun setupViewBinding(inflater: LayoutInflater, parent: LinearLayout): KeyboardFormBinding { - return KeyboardFormBinding.inflate(LayoutInflater.from(context), this, true) + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { + + var currentInputConnection: InputConnection? = null + var onBackClick: (() -> Unit)? = null + + private val composeView = ComposeView(context).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool) + setContent { + FrogoKeyboardTheme { + FormKeyboardScreen( + onCommitText = { text -> + currentInputConnection?.commitText(text, 1) + }, + onBackClick = { + onBackClick?.invoke() + } + ) + } + } } - override fun initUI() { - super.initUI() - binding.apply { - btnSubmit.setOnClickListener { - val title = etText.text?.toString()?.trim() - val details = etText2.text?.toString()?.trim() - val number = etText3.text?.toString()?.trim() - - val sb = StringBuilder() - if (!title.isNullOrBlank()) sb.append("Subject: $title\n") - if (!details.isNullOrBlank()) sb.append("Details: $details\n") - if (!number.isNullOrBlank()) sb.append("Ref/No: $number\n") - - if (sb.isNotEmpty()) { - currentInputConnection?.commitText(sb.toString(), 1) - } + init { + addView(composeView) + } - KeyboardNavigationHelper.navigateTo(this@FormKeyboard, destinationKeyboard) - } + fun setInputConnection(inputConnection: InputConnection?) { + currentInputConnection = inputConnection + } - btnClear.setOnClickListener { - etText.setText("") - etText2.setText("") - etText3.setText("") - } - } + fun setOnBackClickListener(listener: () -> Unit) { + onBackClick = listener } } \ No newline at end of file diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboardScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboardScreen.kt new file mode 100644 index 0000000..ab36f22 --- /dev/null +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboardScreen.kt @@ -0,0 +1,186 @@ +package com.frogobox.appkeyboard.ui.keyboard.form + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.frogobox.appkeyboard.ui.keyboard.common.KeyboardFeatureToolbar + +@Composable +fun FormKeyboardScreen( + onCommitText: (String) -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier +) { + var subject by remember { mutableStateOf("") } + var details by remember { mutableStateOf("") } + var refNumber by remember { mutableStateOf("") } + + val focusManager = LocalFocusManager.current + val scrollState = rememberScrollState() + + Column( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + ) { + KeyboardFeatureToolbar( + title = "Quick Form Snippet", + subtitle = "Fill and insert structured message", + onBackClick = onBackClick + ) + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .padding(horizontal = 12.dp, vertical = 8.dp) + ) { + // Subject Field + OutlinedTextField( + value = subject, + onValueChange = { subject = it }, + label = { Text("Title / Subject", fontSize = 12.sp) }, + singleLine = true, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next + ), + keyboardActions = KeyboardActions( + onNext = { focusManager.moveFocus(FocusDirection.Down) } + ), + shape = RoundedCornerShape(10.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + focusedLabelColor = MaterialTheme.colorScheme.primary + ), + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Details Field + OutlinedTextField( + value = details, + onValueChange = { details = it }, + label = { Text("Details / Full Description", fontSize = 12.sp) }, + maxLines = 3, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next + ), + keyboardActions = KeyboardActions( + onNext = { focusManager.moveFocus(FocusDirection.Down) } + ), + shape = RoundedCornerShape(10.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + focusedLabelColor = MaterialTheme.colorScheme.primary + ), + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Ref Number Field + OutlinedTextField( + value = refNumber, + onValueChange = { refNumber = it }, + label = { Text("Number / Amount / Code", fontSize = 12.sp) }, + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { focusManager.clearFocus() } + ), + shape = RoundedCornerShape(10.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + focusedLabelColor = MaterialTheme.colorScheme.primary + ), + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(12.dp)) + + // Action Buttons Row + Row( + modifier = Modifier.fillMaxWidth() + ) { + OutlinedButton( + onClick = { + subject = "" + details = "" + refNumber = "" + }, + modifier = Modifier + .weight(1f) + .height(44.dp), + shape = RoundedCornerShape(12.dp) + ) { + Text("Clear", fontSize = 12.sp) + } + + Spacer(modifier = Modifier.width(8.dp)) + + Button( + onClick = { + val sb = StringBuilder() + if (subject.isNotBlank()) sb.append("Subject: ${subject.trim()}\n") + if (details.isNotBlank()) sb.append("Details: ${details.trim()}\n") + if (refNumber.isNotBlank()) sb.append("Ref/No: ${refNumber.trim()}\n") + + if (sb.isNotEmpty()) { + onCommitText(sb.toString()) + } + }, + modifier = Modifier + .weight(2f) + .height(44.dp), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary + ) + ) { + Text( + "Insert to Chat", + fontSize = 12.sp, + fontWeight = FontWeight.Bold + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + } + } +} diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/movie/MovieKeyboard.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/movie/MovieKeyboard.kt index 36b430e..54d65d7 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/movie/MovieKeyboard.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/movie/MovieKeyboard.kt @@ -2,147 +2,93 @@ package com.frogobox.appkeyboard.ui.keyboard.movie import android.content.Context import android.util.AttributeSet -import android.util.Log -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.LinearLayout -import com.bumptech.glide.Glide +import android.view.inputmethod.InputConnection +import android.widget.FrameLayout +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import com.frogobox.api.movie.ConsumeMovieApi -import com.frogobox.appkeyboard.databinding.ItemKeyboardMovieBinding -import com.frogobox.appkeyboard.databinding.KeyboardGridBinding import com.frogobox.coresdk.response.FrogoDataResponse import com.frogobox.coreutil.movie.MovieUrl import com.frogobox.coreutil.movie.model.TrendingMovie import com.frogobox.coreutil.movie.response.Trending -import com.frogobox.libkeyboard.common.core.BaseKeyboard -import com.frogobox.recycler.core.FrogoRecyclerNotifyListener -import com.frogobox.recycler.core.IFrogoBindingAdapter -import com.frogobox.recycler.ext.injectorBinding -import com.frogobox.sdk.ext.gone -import com.frogobox.sdk.ext.visible +import com.frogobox.appkeyboard.ui.theme.compose.FrogoKeyboardTheme +import kotlinx.coroutines.flow.MutableStateFlow /** - * Created by Faisal Amir on 07/11/22 - * ----------------------------------------- - * E-mail : faisalamircs@gmail.com - * Github : github.com/amirisback - * ----------------------------------------- - * Copyright (C) Frogobox ID / amirisback - * All rights reserved + * Modern Jetpack Compose-based Trending Movies Keyboard panel. */ - -class MovieKeyboard( +class MovieKeyboard @JvmOverloads constructor( context: Context, - attrs: AttributeSet?, -) : BaseKeyboard(context, attrs) { + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { + + private val movieListState = MutableStateFlow>(emptyList()) + private val isLoadingState = MutableStateFlow(false) + + var currentInputConnection: InputConnection? = null + var onBackClick: (() -> Unit)? = null + + private val composeView = ComposeView(context).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool) + setContent { + FrogoKeyboardTheme { + val movies by movieListState.collectAsState() + val loading by isLoadingState.collectAsState() + + MovieKeyboardScreen( + movieList = movies, + isLoading = loading, + onCommitText = { text -> + currentInputConnection?.commitText(text, 1) + }, + onBackClick = { + onBackClick?.invoke() + } + ) + } + } + } - override fun setupViewBinding(inflater: LayoutInflater, parent: LinearLayout): KeyboardGridBinding { - return KeyboardGridBinding.inflate(LayoutInflater.from(context), this, true) + init { + addView(composeView) + fetchMovies() } - override fun initUI() { - super.initUI() - binding.apply { - tvToolbarTitle.text = "Trending Movies" - } + fun setInputConnection(inputConnection: InputConnection?) { + currentInputConnection = inputConnection } - override fun initData() { - super.initData() + fun setOnBackClickListener(listener: () -> Unit) { + onBackClick = listener + } - val consumeMovieApi = ConsumeMovieApi(MovieUrl.API_KEY) // Your API_KEY + fun fetchMovies() { + val consumeMovieApi = ConsumeMovieApi(MovieUrl.API_KEY) consumeMovieApi.getTrendingMovieDay(object : FrogoDataResponse> { - override fun onFailed(statusCode: Int, errorMessage: String) {} - override fun onFinish() {} + override fun onFailed(statusCode: Int, errorMessage: String) { + isLoadingState.value = false + } + + override fun onFinish() { + isLoadingState.value = false + } override fun onHideProgress() { - binding.progressBar.gone() + isLoadingState.value = false } override fun onShowProgress() { - binding.progressBar.visible() + isLoadingState.value = true } override fun onSuccess(data: Trending) { - data.results?.let { setupRv(it) } + isLoadingState.value = false + data.results?.let { movieListState.value = it } } - }) } - private fun setupRv(data: List) { - binding.apply { - - val adapterCallback = - object : IFrogoBindingAdapter { - override fun onItemClicked( - binding: ItemKeyboardMovieBinding, - data: TrendingMovie, - position: Int, - notifyListener: FrogoRecyclerNotifyListener, - ) { - // Your Clicked - Log.d("FrogoKeyboard", "onItemClicked: ${data.title}") - Log.d("FrogoKeyboard", "currentInputConnection: $currentInputConnection") - val output = "Movie: ${data.title} - ${data.release_date} \n " + - "Overview: ${data.overview} \n " + - "Vote: ${data.vote_average} \n " + - "${MovieUrl.BASE_URL_IMAGE_ORIGINAL}${data.poster_path}" + - "\n" + - "Terima Kasih" - currentInputConnection?.commitText(output, 1) - } - - override fun onItemLongClicked( - binding: ItemKeyboardMovieBinding, - data: TrendingMovie, - position: Int, - notifyListener: FrogoRecyclerNotifyListener, - ) { - } - - override fun areContentsTheSame( - oldItem: TrendingMovie, - newItem: TrendingMovie - ): Boolean { - return oldItem == newItem - } - - override fun areItemsTheSame( - oldItem: TrendingMovie, - newItem: TrendingMovie - ): Boolean { - return oldItem.id == newItem.id - } - - override fun setViewBinding(parent: ViewGroup): ItemKeyboardMovieBinding { - return ItemKeyboardMovieBinding.inflate(LayoutInflater.from(context), - parent, - false) - } - - override fun setupInitComponent( - binding: ItemKeyboardMovieBinding, - data: TrendingMovie, - position: Int, - notifyListener: FrogoRecyclerNotifyListener, - ) { - binding.apply { - frogoRvGridType6TvTitle.text = data.title - frogoRvGridType6TvSubtitle.text = data.release_date - frogoRvGridType6TvDesc.text = data.overview - frogoRvGridType6TvRating.text = String.format("%.1f", data.vote_average) - Glide.with(context).load("${MovieUrl.BASE_URL_IMAGE_ORIGINAL}${data.poster_path}").into(frogoRvGridType6CivPoster) - } - } - } - - rvKeyboardMain.injectorBinding() - .addData(data) - .createLayoutStaggeredGrid(2) - .addCallback(adapterCallback) - .build() - } - } - } \ No newline at end of file diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/movie/MovieKeyboardScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/movie/MovieKeyboardScreen.kt new file mode 100644 index 0000000..673750d --- /dev/null +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/movie/MovieKeyboardScreen.kt @@ -0,0 +1,171 @@ +package com.frogobox.appkeyboard.ui.keyboard.movie + +import java.util.Locale +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +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.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.frogobox.coreutil.movie.MovieUrl +import com.frogobox.coreutil.movie.model.TrendingMovie +import com.frogobox.appkeyboard.ui.keyboard.common.AsyncGlideImage +import com.frogobox.appkeyboard.ui.keyboard.common.KeyboardFeatureToolbar + +@Composable +fun MovieKeyboardScreen( + movieList: List, + isLoading: Boolean, + onCommitText: (String) -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + ) { + KeyboardFeatureToolbar( + title = "Trending Movies", + subtitle = "Tap to share movie recommendation", + onBackClick = onBackClick + ) + + if (isLoading) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + color = MaterialTheme.colorScheme.primary, + strokeWidth = 3.dp + ) + } + } else { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items( + items = movieList, + key = { it.id ?: it.hashCode() } + ) { movie -> + val posterUrl = "${MovieUrl.BASE_URL_IMAGE_ORIGINAL}${movie.poster_path ?: ""}" + + Card( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .clickable { + val output = "Movie: ${movie.title ?: ""} - ${movie.release_date ?: ""} \n " + + "Overview: ${movie.overview ?: ""} \n " + + "Vote: ${movie.vote_average ?: 0.0} \n " + + posterUrl + + "\n" + + "Terima Kasih" + onCommitText(output) + }, + shape = RoundedCornerShape(10.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f) + ) + ) { + Column(modifier = Modifier.fillMaxWidth()) { + // Poster + AsyncGlideImage( + url = posterUrl, + contentDescription = movie.title ?: "Movie poster", + modifier = Modifier + .fillMaxWidth() + .height(110.dp), + contentScale = ContentScale.Crop + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + // Title + Text( + text = movie.title ?: "", + style = MaterialTheme.typography.titleSmall.copy( + fontSize = 12.5.sp, + fontWeight = FontWeight.Bold + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Spacer(modifier = Modifier.height(2.dp)) + + // Rating & Release Date + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Star, + contentDescription = "Rating", + tint = Color(0xFFFFB300), + modifier = Modifier.size(12.dp) + ) + Spacer(modifier = Modifier.width(3.dp)) + Text( + text = String.format(Locale.US, "%.1f", movie.vote_average ?: 0.0), + fontSize = 10.5.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = movie.release_date ?: "", + fontSize = 10.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/news/NewsKeyboard.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/news/NewsKeyboard.kt index c3b11b6..c563c42 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/news/NewsKeyboard.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/news/NewsKeyboard.kt @@ -2,61 +2,74 @@ package com.frogobox.appkeyboard.ui.keyboard.news import android.content.Context import android.util.AttributeSet -import android.util.Log -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.LinearLayout +import android.view.inputmethod.InputConnection +import android.widget.FrameLayout +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import com.frogobox.api.news.ConsumeNewsApi -import com.frogobox.appkeyboard.databinding.ItemKeyboardNewsBinding -import com.frogobox.appkeyboard.databinding.KeyboardListBinding import com.frogobox.coreapi.ConsumeApiResponse import com.frogobox.coreutil.news.NewsConstant.CATEGORY_HEALTH import com.frogobox.coreutil.news.NewsConstant.COUNTRY_ID import com.frogobox.coreutil.news.NewsUrl import com.frogobox.coreutil.news.model.Article import com.frogobox.coreutil.news.response.ArticleResponse - -import com.bumptech.glide.Glide -import com.frogobox.libkeyboard.common.core.BaseKeyboard -import com.frogobox.recycler.core.FrogoRecyclerNotifyListener -import com.frogobox.recycler.core.IFrogoBindingAdapter -import com.frogobox.recycler.ext.injectorBinding -import com.frogobox.sdk.ext.gone -import com.frogobox.sdk.ext.visible +import com.frogobox.appkeyboard.ui.theme.compose.FrogoKeyboardTheme +import kotlinx.coroutines.flow.MutableStateFlow /** - * Created by Faisal Amir on 07/11/22 - * ----------------------------------------- - * E-mail : faisalamircs@gmail.com - * Github : github.com/amirisback - * ----------------------------------------- - * Copyright (C) Frogobox ID / amirisback - * All rights reserved + * Modern Jetpack Compose-based News Headlines Keyboard panel. */ - -class NewsKeyboard( +class NewsKeyboard @JvmOverloads constructor( context: Context, - attrs: AttributeSet?, -) : BaseKeyboard(context, attrs) { + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { + + private val articleListState = MutableStateFlow>(emptyList()) + private val isLoadingState = MutableStateFlow(false) + + var currentInputConnection: InputConnection? = null + var onBackClick: (() -> Unit)? = null + + private val composeView = ComposeView(context).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool) + setContent { + FrogoKeyboardTheme { + val articles by articleListState.collectAsState() + val loading by isLoadingState.collectAsState() + + NewsKeyboardScreen( + articles = articles, + isLoading = loading, + onCommitText = { text -> + currentInputConnection?.commitText(text, 1) + }, + onBackClick = { + onBackClick?.invoke() + } + ) + } + } + } - override fun setupViewBinding(inflater: LayoutInflater, parent: LinearLayout): KeyboardListBinding { - return KeyboardListBinding.inflate(LayoutInflater.from(context), this, true) + init { + addView(composeView) + fetchNews() } - override fun onCreate() { - setupData() - initView() + fun setInputConnection(inputConnection: InputConnection?) { + currentInputConnection = inputConnection } - private fun initView() { - binding.apply { - tvToolbarTitle.text = "Top Headlines" - } + fun setOnBackClickListener(listener: () -> Unit) { + onBackClick = listener } - private fun setupData() { - val consumeNewsApi = ConsumeNewsApi(NewsUrl.API_KEY) // Your API_KEY - consumeNewsApi.getTopHeadline( // Adding Base Parameter on main function + fun fetchNews() { + val consumeNewsApi = ConsumeNewsApi(NewsUrl.API_KEY) + consumeNewsApi.getTopHeadline( null, null, CATEGORY_HEALTH, @@ -65,105 +78,27 @@ class NewsKeyboard( null, object : ConsumeApiResponse { override fun onSuccess(data: ArticleResponse) { - // Your Ui or data - data.articles?.let { setupRv(it) } + isLoadingState.value = false + data.articles?.let { articleListState.value = it } } override fun onFailed(statusCode: Int, errorMessage: String) { - // Your failed to do + isLoadingState.value = false } override fun onFinish() { - // Your finish to do + isLoadingState.value = false } override fun onShowProgress() { - // Your Progress Show - binding.progressBar.visible() + isLoadingState.value = true } override fun onHideProgress() { - // Your Progress Hide - binding.progressBar.gone() - } - - }) - - } - - private fun setupRv(data: List
) { - binding.apply { - - val adapterCallback = object : IFrogoBindingAdapter { - override fun onItemClicked( - binding: ItemKeyboardNewsBinding, - data: Article, - position: Int, - notifyListener: FrogoRecyclerNotifyListener
, - ) { - // Your Clicked - Log.d("NewsKeyboard", "onItemClicked: ${data.title}") - Log.d("FrogoKeyboard", "currentInputConnection: $currentInputConnection") - val output = "${data.title}\n" + - "${data.author}\n" + - "\n" + - "${data.description}\n" + - "\n" + - "Sumber : ${data.url}" + - "\n" + - "Terima Kasih" - currentInputConnection?.commitText(output, 1) - } - - override fun onItemLongClicked( - binding: ItemKeyboardNewsBinding, - data: Article, - position: Int, - notifyListener: FrogoRecyclerNotifyListener
, - ) { - } - - override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean { - return oldItem == newItem - } - - override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean { - return oldItem.title == newItem.title - } - - override fun setViewBinding(parent: ViewGroup): ItemKeyboardNewsBinding { - return ItemKeyboardNewsBinding.inflate(LayoutInflater.from(context), - parent, - false) - } - - override fun setupInitComponent( - binding: ItemKeyboardNewsBinding, - data: Article, - position: Int, - notifyListener: FrogoRecyclerNotifyListener
, - ) { - binding.apply { - tvItemKeyboardMain.text = data.title - tvItemKeyboardDesc.text = data.description ?: "" - tvItemKeyboardSource.text = if (!data.author.isNullOrBlank()) data.author else "News" - if (!data.urlToImage.isNullOrBlank()) { - Glide.with(context) - .load(data.urlToImage) - .into(ivItemKeyboardNews) - } else { - ivItemKeyboardNews.setImageResource(com.frogobox.appkeyboard.R.drawable.ic_menu_news) - } - } + isLoadingState.value = false } } - - rvKeyboardMain.injectorBinding() - .addData(data) - .createLayoutLinearVertical(false) - .addCallback(adapterCallback) - .build() - } + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/news/NewsKeyboardScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/news/NewsKeyboardScreen.kt new file mode 100644 index 0000000..9850f4b --- /dev/null +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/news/NewsKeyboardScreen.kt @@ -0,0 +1,161 @@ +package com.frogobox.appkeyboard.ui.keyboard.news + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.frogobox.coreutil.news.model.Article +import com.frogobox.appkeyboard.ui.keyboard.common.AsyncGlideImage +import com.frogobox.appkeyboard.ui.keyboard.common.KeyboardFeatureToolbar + +@Composable +fun NewsKeyboardScreen( + articles: List
, + isLoading: Boolean, + onCommitText: (String) -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + ) { + KeyboardFeatureToolbar( + title = "Top Headlines", + subtitle = "Tap to share article & source link", + onBackClick = onBackClick + ) + + if (isLoading) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + color = MaterialTheme.colorScheme.primary, + strokeWidth = 3.dp + ) + } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items( + items = articles, + key = { it.url ?: it.title ?: "" } + ) { article -> + Card( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .clickable { + val output = "${article.title}\n" + + "${article.author ?: "News"}\n" + + "\n" + + "${article.description ?: ""}\n" + + "\n" + + "Sumber : ${article.url}" + + "\n" + + "Terima Kasih" + onCommitText(output) + }, + shape = RoundedCornerShape(10.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f) + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (!article.urlToImage.isNullOrBlank()) { + AsyncGlideImage( + url = article.urlToImage, + contentDescription = article.title, + modifier = Modifier + .size(68.dp) + .clip(RoundedCornerShape(8.dp)), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.width(10.dp)) + } + + Column(modifier = Modifier.weight(1f)) { + Text( + text = article.title ?: "", + style = MaterialTheme.typography.titleSmall.copy( + fontSize = 12.5.sp, + fontWeight = FontWeight.Bold + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + + Spacer(modifier = Modifier.height(2.dp)) + + Text( + text = if (!article.author.isNullOrBlank()) article.author!! else "Top News", + style = MaterialTheme.typography.bodySmall.copy( + fontSize = 10.5.sp, + fontWeight = FontWeight.SemiBold + ), + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + if (!article.description.isNullOrBlank()) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = article.description!!, + style = MaterialTheme.typography.bodySmall.copy( + fontSize = 10.5.sp + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboard.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboard.kt index ed91526..d8b34f4 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboard.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboard.kt @@ -2,230 +2,79 @@ package com.frogobox.appkeyboard.ui.keyboard.templatetext import android.content.Context import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.LinearLayout -import androidx.core.content.ContextCompat -import com.frogobox.appkeyboard.R -import com.frogobox.appkeyboard.databinding.ItemKeyboardTemplateCategoryBinding -import com.frogobox.appkeyboard.databinding.ItemKeyboardTemplateTextBinding -import com.frogobox.appkeyboard.databinding.KeyboardTemplateTextBinding +import android.view.inputmethod.InputConnection +import android.widget.FrameLayout +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import com.frogobox.appkeyboard.model.KeyboardFeatureType -import com.frogobox.appkeyboard.model.KeyboardFeatureType.TEMPLATE_TEXT_APP -import com.frogobox.appkeyboard.model.KeyboardFeatureType.TEMPLATE_TEXT_GAME -import com.frogobox.appkeyboard.model.KeyboardFeatureType.TEMPLATE_TEXT_GREETING -import com.frogobox.appkeyboard.model.KeyboardFeatureType.TEMPLATE_TEXT_LOVE -import com.frogobox.appkeyboard.model.KeyboardFeatureType.TEMPLATE_TEXT_SALE -import com.frogobox.appkeyboard.model.TemplateText -import com.frogobox.libkeyboard.common.core.BaseKeyboard -import com.frogobox.recycler.core.FrogoRecyclerNotifyListener -import com.frogobox.recycler.core.IFrogoBindingAdapter -import com.frogobox.recycler.ext.injectorBinding +import com.frogobox.appkeyboard.ui.theme.compose.FrogoKeyboardTheme +import kotlinx.coroutines.flow.MutableStateFlow data class TemplateCategoryItem( val type: KeyboardFeatureType, val icon: String, val title: String, - val isSelected: Boolean + val isSelected: Boolean = false ) -class TemplateTextKeyboard( +/** + * Modern Jetpack Compose-based TemplateText Keyboard panel. + */ +class TemplateTextKeyboard @JvmOverloads constructor( context: Context, - attrs: AttributeSet?, -) : BaseKeyboard(context, attrs) { + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { companion object { val CATEGORIES = listOf( - Pair(TEMPLATE_TEXT_GAME, Pair("🎮", "Game")), - Pair(TEMPLATE_TEXT_APP, Pair("📱", "App")), - Pair(TEMPLATE_TEXT_SALE, Pair("💰", "Sale")), - Pair(TEMPLATE_TEXT_GREETING, Pair("👋", "Greeting")), - Pair(TEMPLATE_TEXT_LOVE, Pair("❤️", "Love")) + Pair(KeyboardFeatureType.TEMPLATE_TEXT_GAME, Pair("🎮", "Game")), + Pair(KeyboardFeatureType.TEMPLATE_TEXT_APP, Pair("📱", "App")), + Pair(KeyboardFeatureType.TEMPLATE_TEXT_SALE, Pair("💰", "Sale")), + Pair(KeyboardFeatureType.TEMPLATE_TEXT_GREETING, Pair("👋", "Greeting")), + Pair(KeyboardFeatureType.TEMPLATE_TEXT_LOVE, Pair("❤️", "Love")) ) } - private var currentType: KeyboardFeatureType? = null - - override fun setupViewBinding(inflater: LayoutInflater, parent: LinearLayout): KeyboardTemplateTextBinding { - return KeyboardTemplateTextBinding.inflate(LayoutInflater.from(context), this, true) - } - - override fun initUI() { - super.initUI() - val type = currentType ?: TEMPLATE_TEXT_GAME - currentType = type - setupCategories(type) - setupContent(type) - } - - fun setupTemplateTextType(templateTextType: KeyboardFeatureType) { - this.currentType = templateTextType - setupCategories(templateTextType) - setupContent(templateTextType) - } - - private fun setupCategories(selectedType: KeyboardFeatureType) { - val categoryData = CATEGORIES.map { (type, meta) -> - TemplateCategoryItem( - type = type, - icon = meta.first, - title = meta.second, - isSelected = type == selectedType - ) - } - - val callback = object : IFrogoBindingAdapter { - override fun areContentsTheSame(oldItem: TemplateCategoryItem, newItem: TemplateCategoryItem): Boolean { - return oldItem == newItem - } - - override fun areItemsTheSame(oldItem: TemplateCategoryItem, newItem: TemplateCategoryItem): Boolean { - return oldItem.type == newItem.type - } - - override fun setViewBinding(parent: ViewGroup): ItemKeyboardTemplateCategoryBinding { - return ItemKeyboardTemplateCategoryBinding.inflate(LayoutInflater.from(context), parent, false) - } - - override fun setupInitComponent( - binding: ItemKeyboardTemplateCategoryBinding, - data: TemplateCategoryItem, - position: Int, - notifyListener: FrogoRecyclerNotifyListener - ) { - binding.apply { - tvCategoryIcon.text = data.icon - tvCategoryName.text = data.title - - if (data.isSelected) { - llCategoryContainer.setBackgroundResource(R.drawable.bg_feature_chip_active) - tvCategoryName.setTextColor(ContextCompat.getColor(context, R.color.color_feature_chip_active_text)) - } else { - llCategoryContainer.setBackgroundResource(R.drawable.bg_feature_chip_inactive) - tvCategoryName.setTextColor(ContextCompat.getColor(context, R.color.color_feature_chip_text)) + private val currentCategoryState = MutableStateFlow(KeyboardFeatureType.TEMPLATE_TEXT_GAME) + + var currentInputConnection: InputConnection? = null + var onBackClick: (() -> Unit)? = null + + private val composeView = ComposeView(context).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool) + setContent { + FrogoKeyboardTheme { + val category by currentCategoryState.collectAsState() + TemplateTextKeyboardScreen( + initialType = category, + onCommitText = { text -> + currentInputConnection?.commitText(text, 1) + }, + onBackClick = { + onBackClick?.invoke() } - } - } - - override fun onItemClicked( - binding: ItemKeyboardTemplateCategoryBinding, - data: TemplateCategoryItem, - position: Int, - notifyListener: FrogoRecyclerNotifyListener - ) { - if (currentType != data.type) { - currentType = data.type - setupCategories(data.type) - setupContent(data.type) - } + ) } } - - binding.rvTemplateCategories.injectorBinding() - .addData(categoryData) - .createLayoutLinearHorizontal(false) - .addCallback(callback) - .build() } - private fun setupContent(templateTextType: KeyboardFeatureType) { - val title: String - val list: List - - when (templateTextType) { - TEMPLATE_TEXT_GAME -> { - title = "Game Templates" - list = TemplateTextUtils.getTextGame(context) - } - - TEMPLATE_TEXT_APP -> { - title = "App Templates" - list = TemplateTextUtils.getTextApp(context) - } - - TEMPLATE_TEXT_SALE -> { - title = "Sale / Store Templates" - list = TemplateTextUtils.getTextSale(context) - } - - TEMPLATE_TEXT_GREETING -> { - title = "Greeting Templates" - list = TemplateTextUtils.getTextGreeting(context) - } - - TEMPLATE_TEXT_LOVE -> { - title = "Love & Sweet Templates" - list = TemplateTextUtils.getTextLove(context) - } - - else -> { - title = "Quick Templates" - list = listOf() - } - } - - binding.tvToolbarTitle.text = title - setupRv(list) + init { + addView(composeView) } - private fun setupRv(data: List) { - binding.apply { - val adapterCallback = object : - IFrogoBindingAdapter { - override fun onItemClicked( - binding: ItemKeyboardTemplateTextBinding, - data: TemplateText, - position: Int, - notifyListener: FrogoRecyclerNotifyListener, - ) { - currentInputConnection?.commitText(data.text, 1) - } - - override fun onItemLongClicked( - binding: ItemKeyboardTemplateTextBinding, - data: TemplateText, - position: Int, - notifyListener: FrogoRecyclerNotifyListener, - ) {} - - override fun areContentsTheSame( - oldItem: TemplateText, - newItem: TemplateText - ): Boolean { - return oldItem == newItem - } - - override fun areItemsTheSame( - oldItem: TemplateText, - newItem: TemplateText - ): Boolean { - return oldItem.id == newItem.id - } - - override fun setViewBinding(parent: ViewGroup): ItemKeyboardTemplateTextBinding { - return ItemKeyboardTemplateTextBinding.inflate( - LayoutInflater.from(context), - parent, - false - ) - } + fun setupTemplateTextType(templateTextType: KeyboardFeatureType) { + currentCategoryState.value = templateTextType + } - override fun setupInitComponent( - binding: ItemKeyboardTemplateTextBinding, - data: TemplateText, - position: Int, - notifyListener: FrogoRecyclerNotifyListener, - ) { - binding.tvItemTemplateText.text = data.text - } - } + fun setInputConnection(inputConnection: InputConnection?) { + currentInputConnection = inputConnection + } - rvKeyboardMain.injectorBinding() - .addData(data) - .createLayoutLinearVertical(false) - .addCallback(adapterCallback) - .build() - } + fun setOnBackClickListener(listener: () -> Unit) { + onBackClick = listener } } \ No newline at end of file diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboardScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboardScreen.kt new file mode 100644 index 0000000..890f252 --- /dev/null +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboardScreen.kt @@ -0,0 +1,162 @@ +package com.frogobox.appkeyboard.ui.keyboard.templatetext + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.frogobox.appkeyboard.model.KeyboardFeatureType +import com.frogobox.appkeyboard.model.KeyboardFeatureType.TEMPLATE_TEXT_APP +import com.frogobox.appkeyboard.model.KeyboardFeatureType.TEMPLATE_TEXT_GAME +import com.frogobox.appkeyboard.model.KeyboardFeatureType.TEMPLATE_TEXT_GREETING +import com.frogobox.appkeyboard.model.KeyboardFeatureType.TEMPLATE_TEXT_LOVE +import com.frogobox.appkeyboard.model.KeyboardFeatureType.TEMPLATE_TEXT_SALE +import com.frogobox.appkeyboard.ui.keyboard.common.KeyboardFeatureToolbar + +data class TemplateCategory( + val type: KeyboardFeatureType, + val icon: String, + val title: String +) + +val CATEGORIES = listOf( + TemplateCategory(TEMPLATE_TEXT_GAME, "🎮", "Game"), + TemplateCategory(TEMPLATE_TEXT_APP, "📱", "App"), + TemplateCategory(TEMPLATE_TEXT_SALE, "💰", "Sale"), + TemplateCategory(TEMPLATE_TEXT_GREETING, "👋", "Greeting"), + TemplateCategory(TEMPLATE_TEXT_LOVE, "❤️", "Love") +) + +@Composable +fun TemplateTextKeyboardScreen( + onCommitText: (String) -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + initialType: KeyboardFeatureType = TEMPLATE_TEXT_GAME +) { + val context = LocalContext.current + var selectedType by remember(initialType) { mutableStateOf(initialType) } + + val currentCategoryTitle = when (selectedType) { + TEMPLATE_TEXT_GAME -> "Game Templates" + TEMPLATE_TEXT_APP -> "App Templates" + TEMPLATE_TEXT_SALE -> "Sale / Store Templates" + TEMPLATE_TEXT_GREETING -> "Greeting Templates" + TEMPLATE_TEXT_LOVE -> "Love & Sweet Templates" + else -> "Quick Templates" + } + + val templateList = remember(selectedType) { + when (selectedType) { + TEMPLATE_TEXT_GAME -> TemplateTextUtils.getTextGame(context) + TEMPLATE_TEXT_APP -> TemplateTextUtils.getTextApp(context) + TEMPLATE_TEXT_SALE -> TemplateTextUtils.getTextSale(context) + TEMPLATE_TEXT_GREETING -> TemplateTextUtils.getTextGreeting(context) + TEMPLATE_TEXT_LOVE -> TemplateTextUtils.getTextLove(context) + else -> emptyList() + } + } + + Column( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + ) { + KeyboardFeatureToolbar( + title = currentCategoryTitle, + subtitle = "Tap any template to paste into chat", + onBackClick = onBackClick + ) + + // Categories Chip Row + LazyRow( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + contentPadding = PaddingValues(horizontal = 10.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + items(CATEGORIES) { category -> + val isSelected = category.type == selectedType + FilterChip( + selected = isSelected, + onClick = { selectedType = category.type }, + label = { + Text( + text = "${category.icon} ${category.title}", + fontSize = 12.sp, + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal + ) + }, + shape = RoundedCornerShape(16.dp), + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = MaterialTheme.colorScheme.primaryContainer, + selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + ) + } + } + + HorizontalDivider( + thickness = 0.8.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f) + ) + + // Template List + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 10.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + items( + items = templateList, + key = { it.id } + ) { template -> + Card( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .clickable { onCommitText(template.text) }, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f) + ), + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = template.text, + style = MaterialTheme.typography.bodyMedium.copy( + fontSize = 12.5.sp + ), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp) + ) + } + } + } + } +} diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextUtils.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextUtils.kt index 8fbe299..bd41991 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextUtils.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextUtils.kt @@ -4,7 +4,6 @@ import android.content.Context import com.frogobox.appkeyboard.model.KeyboardFeatureType import com.frogobox.appkeyboard.model.TemplateText import com.frogobox.sdk.ext.getDataFromJsonAsset -import com.frogobox.sdk.util.FrogoFunc /** * Created by Faisal Amir on 24/10/22 diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/webview/WebiewKeyboard.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/webview/WebiewKeyboard.kt index 0c075d3..81a48e0 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/webview/WebiewKeyboard.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/webview/WebiewKeyboard.kt @@ -1,102 +1,51 @@ package com.frogobox.appkeyboard.ui.keyboard.webview -import android.annotation.SuppressLint import android.content.Context -import android.graphics.Bitmap import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.inputmethod.EditorInfo -import android.webkit.WebChromeClient -import android.webkit.WebView -import android.webkit.WebViewClient -import android.widget.LinearLayout -import com.frogobox.appkeyboard.databinding.KeyboardWebviewBinding -import com.frogobox.libkeyboard.common.core.BaseKeyboard -import com.frogobox.sdk.ext.gone -import com.frogobox.sdk.ext.visible +import android.view.inputmethod.InputConnection +import android.widget.FrameLayout +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import com.frogobox.appkeyboard.ui.theme.compose.FrogoKeyboardTheme /** - * Created by Faisal Amir on 07/11/22 - * ----------------------------------------- - * E-mail : faisalamircs@gmail.com - * Github : github.com/amirisback - * ----------------------------------------- - * Copyright (C) Frogobox ID / amirisback - * All rights reserved + * Modern Jetpack Compose-based Webview Keyboard panel. */ - -class WebiewKeyboard( +class WebiewKeyboard @JvmOverloads constructor( context: Context, - attrs: AttributeSet?, -) : BaseKeyboard(context, attrs) { - - override fun setupViewBinding(inflater: LayoutInflater, parent: LinearLayout): KeyboardWebviewBinding { - return KeyboardWebviewBinding.inflate(LayoutInflater.from(context), this, true) - } - - @SuppressLint("SetJavaScriptEnabled") - override fun initUI() { - super.initUI() - binding.apply { - webview.settings.javaScriptEnabled = true - webview.settings.domStorageEnabled = true - - webview.webViewClient = object : WebViewClient() { - override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { - super.onPageStarted(view, url, favicon) - pbWebLoading.visible() - url?.let { etSearchUrl.setText(it) } - } - - override fun onPageFinished(view: WebView?, url: String?) { - super.onPageFinished(view, url) - pbWebLoading.gone() - } - } - - webview.webChromeClient = object : WebChromeClient() { - override fun onProgressChanged(view: WebView?, newProgress: Int) { - super.onProgressChanged(view, newProgress) - pbWebLoading.progress = newProgress - if (newProgress >= 100) { - pbWebLoading.gone() - } else { - pbWebLoading.visible() + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { + + var currentInputConnection: InputConnection? = null + var onBackClick: (() -> Unit)? = null + + private val composeView = ComposeView(context).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool) + setContent { + FrogoKeyboardTheme { + WebviewKeyboardScreen( + onCommitText = { text -> + currentInputConnection?.commitText(text, 1) + }, + onBackClick = { + onBackClick?.invoke() } - } - } - - val loadUrlOrSearch = { - val query = etSearchUrl.text?.toString()?.trim() ?: "" - val targetUrl = when { - query.startsWith("http://") || query.startsWith("https://") -> query - query.contains(".") && !query.contains(" ") -> "https://$query" - query.isNotEmpty() -> "https://www.google.com/search?q=" + java.net.URLEncoder.encode(query, "UTF-8") - else -> "https://www.google.com" - } - webview.loadUrl(targetUrl) - } - - etSearchUrl.setOnEditorActionListener { _, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == EditorInfo.IME_ACTION_GO) { - loadUrlOrSearch() - true - } else false + ) } + } + } - btnReload.setOnClickListener { - webview.reload() - } + init { + addView(composeView) + } - btnInsertLink.setOnClickListener { - val currentUrl = webview.url - if (!currentUrl.isNullOrBlank()) { - currentInputConnection?.commitText(currentUrl, 1) - } - } + fun setInputConnection(inputConnection: InputConnection?) { + currentInputConnection = inputConnection + } - webview.loadUrl("https://www.google.com") - } + fun setOnBackClickListener(listener: () -> Unit) { + onBackClick = listener } } \ No newline at end of file diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/webview/WebviewKeyboardScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/webview/WebviewKeyboardScreen.kt new file mode 100644 index 0000000..a5e6283 --- /dev/null +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/webview/WebviewKeyboardScreen.kt @@ -0,0 +1,259 @@ +package com.frogobox.appkeyboard.ui.keyboard.webview + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.view.ViewGroup +import android.webkit.WebChromeClient +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.ContentPaste +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import java.net.URLEncoder + +@SuppressLint("SetJavaScriptEnabled") +@Composable +fun WebviewKeyboardScreen( + onCommitText: (String) -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier +) { + var urlInput by remember { mutableStateOf("https://www.google.com") } + var currentWebUrl by remember { mutableStateOf("https://www.google.com") } + var webProgress by remember { mutableIntStateOf(0) } + var webViewInstance by remember { mutableStateOf(null) } + + val focusManager = LocalFocusManager.current + + val loadUrlOrSearch: () -> Unit = { + val query = urlInput.trim() + val targetUrl = when { + query.startsWith("http://") || query.startsWith("https://") -> query + query.contains(".") && !query.contains(" ") -> "https://$query" + query.isNotEmpty() -> "https://www.google.com/search?q=" + URLEncoder.encode(query, "UTF-8") + else -> "https://www.google.com" + } + urlInput = targetUrl + webViewInstance?.loadUrl(targetUrl) + focusManager.clearFocus() + } + + Column( + modifier = modifier + .fillMaxWidth() + .height(270.dp) + .background(MaterialTheme.colorScheme.surface) + ) { + // Web Navigation Bar + Row( + modifier = Modifier + .fillMaxWidth() + .height(50.dp) + .padding(horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Back Button + Box( + modifier = Modifier + .size(34.dp) + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + .clickable { onBackClick() } + .padding(6.dp), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.onSurface + ) + } + + Spacer(modifier = Modifier.width(6.dp)) + + // Search / URL input bar + Row( + modifier = Modifier + .weight(1f) + .height(34.dp) + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)) + .padding(horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Search, + contentDescription = "Search", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(16.dp) + ) + + Spacer(modifier = Modifier.width(4.dp)) + + BasicTextField( + value = urlInput, + onValueChange = { urlInput = it }, + singleLine = true, + textStyle = MaterialTheme.typography.bodySmall.copy( + fontSize = 11.5.sp, + color = MaterialTheme.colorScheme.onSurface + ), + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onSearch = { loadUrlOrSearch() }), + modifier = Modifier.weight(1f) + ) + } + + Spacer(modifier = Modifier.width(6.dp)) + + // Reload Button + Box( + modifier = Modifier + .size(34.dp) + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + .clickable { webViewInstance?.reload() } + .padding(6.dp), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "Reload", + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.onSurface + ) + } + + Spacer(modifier = Modifier.width(6.dp)) + + // Paste URL Pill + Row( + modifier = Modifier + .height(30.dp) + .clip(RoundedCornerShape(15.dp)) + .background(MaterialTheme.colorScheme.primary) + .clickable { + val activeUrl = webViewInstance?.url ?: currentWebUrl + if (activeUrl.isNotBlank()) { + onCommitText(activeUrl) + } + } + .padding(horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.ContentPaste, + contentDescription = "Paste", + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.size(12.dp) + ) + Spacer(modifier = Modifier.width(3.dp)) + Text( + text = "Paste", + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onPrimary + ) + } + } + + // Web Loading Progress Bar + if (webProgress in 1..99) { + LinearProgressIndicator( + progress = { webProgress / 100f }, + modifier = Modifier + .fillMaxWidth() + .height(2.5.dp), + color = MaterialTheme.colorScheme.primary + ) + } else { + HorizontalDivider( + thickness = 0.8.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f) + ) + } + + // Native WebView embedded in Compose + AndroidView( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + factory = { ctx -> + WebView(ctx).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + + webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + url?.let { + urlInput = it + currentWebUrl = it + } + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + webProgress = 100 + } + } + + webChromeClient = object : WebChromeClient() { + override fun onProgressChanged(view: WebView?, newProgress: Int) { + super.onProgressChanged(view, newProgress) + webProgress = newProgress + } + } + + loadUrl("https://www.google.com") + webViewInstance = this + } + }, + update = { webViewInstance = it } + ) + } +} diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/language/KeyboardLanguageActivity.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/language/KeyboardLanguageActivity.kt index f5fc81a..ec7e0ae 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/language/KeyboardLanguageActivity.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/language/KeyboardLanguageActivity.kt @@ -4,6 +4,7 @@ import android.os.Bundle import androidx.activity.viewModels import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.frogobox.appkeyboard.common.base.BaseComposeActivity @@ -16,7 +17,7 @@ class KeyboardLanguageActivity : BaseComposeActivity() { private val viewModel: KeyboardLanguageViewModel by viewModels() private var languageList by mutableStateOf>(emptyList()) - private var activeLanguageXml by mutableStateOf(0) + private var activeLanguageXml by mutableIntStateOf(0) override fun onCreateExt(savedInstanceState: Bundle?) { super.onCreateExt(savedInstanceState) diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/language/KeyboardLanguageViewModel.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/language/KeyboardLanguageViewModel.kt index a096b1a..9004140 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/language/KeyboardLanguageViewModel.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/language/KeyboardLanguageViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.frogobox.appkeyboard.common.base.BaseViewModel import com.frogobox.appkeyboard.services.KeyboardUtil -import com.frogobox.coresdk.response.FrogoStateResponse import com.frogobox.sdk.delegate.preference.PreferenceDelegates import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/sound/SoundActivity.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/sound/SoundActivity.kt index 62e609b..b47dfd9 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/sound/SoundActivity.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/sound/SoundActivity.kt @@ -4,6 +4,7 @@ import android.os.Bundle import androidx.activity.viewModels import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.frogobox.appkeyboard.common.base.BaseComposeActivity @@ -17,7 +18,7 @@ class SoundActivity : BaseComposeActivity() { private var soundEnabled by mutableStateOf(true) private var selectedSoundType by mutableStateOf(MechanicalSoundType.CHERRY_MX_BLUE) - private var soundVolume by mutableStateOf(80) + private var soundVolume by mutableIntStateOf(80) private var vibrateEnabled by mutableStateOf(true) override fun onCreateExt(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/sound/SoundScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/sound/SoundScreen.kt index 75b2b8e..1023e73 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/sound/SoundScreen.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/sound/SoundScreen.kt @@ -2,7 +2,6 @@ package com.frogobox.appkeyboard.ui.sound import android.view.HapticFeedbackConstants import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -44,7 +43,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalView import androidx.compose.ui.text.font.FontWeight diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestScreen.kt index 34946a1..8d91be6 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestScreen.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestScreen.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -37,7 +36,6 @@ import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Inbox import androidx.compose.material.icons.filled.Keyboard -import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.PlayCircle import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Search @@ -46,7 +44,6 @@ import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.AssistChip -import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card @@ -58,8 +55,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults -import androidx.compose.material3.FilterChip -import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -78,8 +73,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/theme/ThemeScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/ThemeScreen.kt index b63daf8..29a8e85 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/theme/ThemeScreen.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/ThemeScreen.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -29,17 +28,14 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.Backspace import androidx.compose.material.icons.automirrored.filled.KeyboardReturn import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.Palette import androidx.compose.material.icons.filled.SentimentSatisfied import androidx.compose.material.icons.filled.Settings -import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.TextFields import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button @@ -49,9 +45,7 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -59,7 +53,6 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/FrogoEmptyView.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/FrogoEmptyView.kt index 1d6d5ce..07150d5 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/FrogoEmptyView.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/FrogoEmptyView.kt @@ -25,9 +25,9 @@ import androidx.compose.ui.unit.sp */ @Composable fun FrogoEmptyView( + modifier: Modifier = Modifier, title: String = "NO DATA FOUND", - subtitle: String = "Please insert data", - modifier: Modifier = Modifier + subtitle: String = "Please insert data" ) { Column( modifier = modifier diff --git a/app/src/main/res/drawable/bg_border.xml b/app/src/main/res/drawable/bg_border.xml deleted file mode 100644 index a50cbc9..0000000 --- a/app/src/main/res/drawable/bg_border.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_card_16dp.xml b/app/src/main/res/drawable/bg_card_16dp.xml deleted file mode 100644 index 8591763..0000000 --- a/app/src/main/res/drawable/bg_card_16dp.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_card_8dp.xml b/app/src/main/res/drawable/bg_card_8dp.xml deleted file mode 100644 index f15208e..0000000 --- a/app/src/main/res/drawable/bg_card_8dp.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_feature_action_pill.xml b/app/src/main/res/drawable/bg_feature_action_pill.xml deleted file mode 100644 index 781da28..0000000 --- a/app/src/main/res/drawable/bg_feature_action_pill.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/bg_feature_card_12dp.xml b/app/src/main/res/drawable/bg_feature_card_12dp.xml deleted file mode 100644 index e81c58b..0000000 --- a/app/src/main/res/drawable/bg_feature_card_12dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/bg_feature_chip_active.xml b/app/src/main/res/drawable/bg_feature_chip_active.xml deleted file mode 100644 index 20126d7..0000000 --- a/app/src/main/res/drawable/bg_feature_chip_active.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/bg_feature_chip_inactive.xml b/app/src/main/res/drawable/bg_feature_chip_inactive.xml deleted file mode 100644 index d75f2a0..0000000 --- a/app/src/main/res/drawable/bg_feature_chip_inactive.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/bg_feature_rating_badge.xml b/app/src/main/res/drawable/bg_feature_rating_badge.xml deleted file mode 100644 index e6b08fa..0000000 --- a/app/src/main/res/drawable/bg_feature_rating_badge.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/bg_feature_toolbar_button.xml b/app/src/main/res/drawable/bg_feature_toolbar_button.xml deleted file mode 100644 index 5953cb5..0000000 --- a/app/src/main/res/drawable/bg_feature_toolbar_button.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml deleted file mode 100644 index 5e77081..0000000 --- a/app/src/main/res/drawable/ic_check.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/ic_feature_insert.xml b/app/src/main/res/drawable/ic_feature_insert.xml deleted file mode 100644 index 54dbdd8..0000000 --- a/app/src/main/res/drawable/ic_feature_insert.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_feature_refresh.xml b/app/src/main/res/drawable/ic_feature_refresh.xml deleted file mode 100644 index 6ea380e..0000000 --- a/app/src/main/res/drawable/ic_feature_refresh.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_feature_search.xml b/app/src/main/res/drawable/ic_feature_search.xml deleted file mode 100644 index f83290b..0000000 --- a/app/src/main/res/drawable/ic_feature_search.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_feature_star.xml b/app/src/main/res/drawable/ic_feature_star.xml deleted file mode 100644 index d70e4d4..0000000 --- a/app/src/main/res/drawable/ic_feature_star.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/layout/activity_autotext.xml b/app/src/main/res/layout/activity_autotext.xml deleted file mode 100644 index cb17793..0000000 --- a/app/src/main/res/layout/activity_autotext.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_autotext_detail.xml b/app/src/main/res/layout/activity_autotext_detail.xml deleted file mode 100644 index beb0198..0000000 --- a/app/src/main/res/layout/activity_autotext_detail.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_autotext_editor.xml b/app/src/main/res/layout/activity_autotext_editor.xml deleted file mode 100644 index 215377c..0000000 --- a/app/src/main/res/layout/activity_autotext_editor.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_keyboard_language.xml b/app/src/main/res/layout/activity_keyboard_language.xml deleted file mode 100644 index 5852945..0000000 --- a/app/src/main/res/layout/activity_keyboard_language.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 4a2c273..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,199 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_theme.xml b/app/src/main/res/layout/activity_theme.xml deleted file mode 100644 index b1bd4e4..0000000 --- a/app/src/main/res/layout/activity_theme.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_toggle.xml b/app/src/main/res/layout/activity_toggle.xml deleted file mode 100644 index d655ab4..0000000 --- a/app/src/main/res/layout/activity_toggle.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_autotext.xml b/app/src/main/res/layout/item_autotext.xml deleted file mode 100644 index 8e7fbac..0000000 --- a/app/src/main/res/layout/item_autotext.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_keyboard_autotext.xml b/app/src/main/res/layout/item_keyboard_autotext.xml deleted file mode 100644 index 12924e5..0000000 --- a/app/src/main/res/layout/item_keyboard_autotext.xml +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_keyboard_movie.xml b/app/src/main/res/layout/item_keyboard_movie.xml deleted file mode 100644 index 86bfa30..0000000 --- a/app/src/main/res/layout/item_keyboard_movie.xml +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_keyboard_multi_form.xml b/app/src/main/res/layout/item_keyboard_multi_form.xml deleted file mode 100644 index f17daff..0000000 --- a/app/src/main/res/layout/item_keyboard_multi_form.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_keyboard_news.xml b/app/src/main/res/layout/item_keyboard_news.xml deleted file mode 100644 index 700d44e..0000000 --- a/app/src/main/res/layout/item_keyboard_news.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_keyboard_template_category.xml b/app/src/main/res/layout/item_keyboard_template_category.xml deleted file mode 100644 index 765a891..0000000 --- a/app/src/main/res/layout/item_keyboard_template_category.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/item_keyboard_template_text.xml b/app/src/main/res/layout/item_keyboard_template_text.xml deleted file mode 100644 index ab3a133..0000000 --- a/app/src/main/res/layout/item_keyboard_template_text.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_language_keyboard.xml b/app/src/main/res/layout/item_language_keyboard.xml deleted file mode 100644 index 2e3e411..0000000 --- a/app/src/main/res/layout/item_language_keyboard.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_spinner.xml b/app/src/main/res/layout/item_spinner.xml deleted file mode 100644 index 1d042e2..0000000 --- a/app/src/main/res/layout/item_spinner.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_theme.xml b/app/src/main/res/layout/item_theme.xml deleted file mode 100644 index 420c4ed..0000000 --- a/app/src/main/res/layout/item_theme.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_toggle.xml b/app/src/main/res/layout/item_toggle.xml deleted file mode 100644 index 39c1bf7..0000000 --- a/app/src/main/res/layout/item_toggle.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/keyboard_autotext.xml b/app/src/main/res/layout/keyboard_autotext.xml deleted file mode 100644 index b23de7b..0000000 --- a/app/src/main/res/layout/keyboard_autotext.xml +++ /dev/null @@ -1,171 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/keyboard_form.xml b/app/src/main/res/layout/keyboard_form.xml deleted file mode 100644 index 158d815..0000000 --- a/app/src/main/res/layout/keyboard_form.xml +++ /dev/null @@ -1,213 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/keyboard_grid.xml b/app/src/main/res/layout/keyboard_grid.xml deleted file mode 100644 index 438d4f2..0000000 --- a/app/src/main/res/layout/keyboard_grid.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/keyboard_list.xml b/app/src/main/res/layout/keyboard_list.xml deleted file mode 100644 index a4669d2..0000000 --- a/app/src/main/res/layout/keyboard_list.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/keyboard_multi_form.xml b/app/src/main/res/layout/keyboard_multi_form.xml deleted file mode 100644 index 1006fc0..0000000 --- a/app/src/main/res/layout/keyboard_multi_form.xml +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/keyboard_template_text.xml b/app/src/main/res/layout/keyboard_template_text.xml deleted file mode 100644 index 04cc8a1..0000000 --- a/app/src/main/res/layout/keyboard_template_text.xml +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/keyboard_webview.xml b/app/src/main/res/layout/keyboard_webview.xml deleted file mode 100644 index 6b1d2fa..0000000 --- a/app/src/main/res/layout/keyboard_webview.xml +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/view_empty.xml b/app/src/main/res/layout/view_empty.xml deleted file mode 100644 index 98c219f..0000000 --- a/app/src/main/res/layout/view_empty.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-night/color.xml b/app/src/main/res/values-night/color.xml index 68a9f83..d84e03b 100644 --- a/app/src/main/res/values-night/color.xml +++ b/app/src/main/res/values-night/color.xml @@ -21,19 +21,7 @@ #37474F - #21262D - #30363D #21262D #F0F6FC - #21262D - #0D47A1 - #4FC3F7 - #C9D1D9 - #8B949E - #16253B - #4FC3F7 - #FFCA28 - #CC000000 - #4FC3F7 \ No newline at end of file diff --git a/app/src/main/res/values/color.xml b/app/src/main/res/values/color.xml index 600c72d..b50e7dc 100644 --- a/app/src/main/res/values/color.xml +++ b/app/src/main/res/values/color.xml @@ -21,19 +21,7 @@ #E0E0E0 - @color/bg_card - #18000000 #0D000000 @color/tv_text - #F1F3F4 - #E1F5FE - #0288D1 - #4A5568 - #718096 - #EBF8FF - #0288D1 - #FFB300 - #B3000000 - #0288D1 \ No newline at end of file diff --git a/app/src/test/java/com/frogobox/appkeyboard/MechanicalSoundAndProgrammerKeyboardTest.kt b/app/src/test/java/com/frogobox/appkeyboard/MechanicalSoundAndProgrammerKeyboardTest.kt index 03a606c..3466ebb 100644 --- a/app/src/test/java/com/frogobox/appkeyboard/MechanicalSoundAndProgrammerKeyboardTest.kt +++ b/app/src/test/java/com/frogobox/appkeyboard/MechanicalSoundAndProgrammerKeyboardTest.kt @@ -4,7 +4,6 @@ import com.frogobox.appkeyboard.ui.language.KeyboardLanguage import com.frogobox.libkeyboard.common.sound.MechanicalSoundType import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Test diff --git a/app/src/test/java/com/frogobox/appkeyboard/ThemeTest.kt b/app/src/test/java/com/frogobox/appkeyboard/ThemeTest.kt index dcfbc7f..4a05d2c 100644 --- a/app/src/test/java/com/frogobox/appkeyboard/ThemeTest.kt +++ b/app/src/test/java/com/frogobox/appkeyboard/ThemeTest.kt @@ -5,7 +5,6 @@ import com.frogobox.appkeyboard.model.KeyboardThemeType import com.frogobox.appkeyboard.model.ThemeType import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Test diff --git a/app/src/test/java/com/frogobox/appkeyboard/WordSuggestionEngineTest.kt b/app/src/test/java/com/frogobox/appkeyboard/WordSuggestionEngineTest.kt index eb9440f..2081575 100644 --- a/app/src/test/java/com/frogobox/appkeyboard/WordSuggestionEngineTest.kt +++ b/app/src/test/java/com/frogobox/appkeyboard/WordSuggestionEngineTest.kt @@ -2,7 +2,6 @@ package com.frogobox.appkeyboard import com.frogobox.appkeyboard.suggestion.WordSuggestionEngine import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Before diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt index 496d857..82571dd 100644 --- a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt +++ b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt @@ -19,7 +19,6 @@ import android.util.AttributeSet import android.util.TypedValue import android.view.Gravity import android.view.HapticFeedbackConstants -import android.view.KeyEvent import android.view.LayoutInflater import android.view.MotionEvent import android.view.View diff --git a/frogo-keyboard/src/test/java/com/frogobox/libkeyboard/BaseKeyboardIMETest.kt b/frogo-keyboard/src/test/java/com/frogobox/libkeyboard/BaseKeyboardIMETest.kt index 2749be5..f6926eb 100644 --- a/frogo-keyboard/src/test/java/com/frogobox/libkeyboard/BaseKeyboardIMETest.kt +++ b/frogo-keyboard/src/test/java/com/frogobox/libkeyboard/BaseKeyboardIMETest.kt @@ -1,14 +1,9 @@ package com.frogobox.libkeyboard import com.frogobox.libkeyboard.common.core.BaseKeyboardIME -import com.frogobox.libkeyboard.common.ext.adjustAlpha -import com.frogobox.libkeyboard.common.ext.darkenColor -import com.frogobox.libkeyboard.common.ext.getContrastColor -import com.frogobox.libkeyboard.common.ext.lightenColor import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotEquals import org.junit.Assert.assertTrue import org.junit.Test diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1369c39..354c539 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,6 +21,7 @@ junit = "4.13.2" junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-work-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" } androidx-lifecycle-compiler = { group = "androidx.lifecycle", name = "lifecycle-compiler", version.ref = "liveCycle" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "liveCycle" } androidx-emoji2 = { group = "androidx.emoji2", name = "emoji2-bundled", version.ref = "emoji2"} androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "roomKtx" } androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "roomKtx" } From 84433f71e4e81e2cbfa89c079f1d6b591a8d6db3 Mon Sep 17 00:00:00 2001 From: amirisback Date: Sun, 20 Sep 2026 08:42:11 +0700 Subject: [PATCH 4/8] feat: implement DataApiService and DataApiRepository for remote data fetching --- app/build.gradle.kts | 21 +- .../appkeyboard/services/KeyboardIME.kt | 6 +- buildSrc/src/main/kotlin/DependencyGradle.kt | 2 +- buildSrc/src/main/kotlin/ProjectSetting.kt | 4 +- .../libkeyboard/ui/emoji/EmojiKeyboard.kt | 221 ------------- gradle.properties | 11 +- gradle/libs.versions.toml | 2 - {frogo-keyboard => lib-keyboard}/.gitignore | 0 .../build.gradle.kts | 18 ++ .../consumer-rules.pro | 0 .../proguard-rules.pro | 0 .../src/main/AndroidManifest.xml | 0 .../src/main/assets/media/_emoji_general.txt | 0 .../main/assets/media/emoji_activities.txt | 0 .../main/assets/media/emoji_animal_nature.txt | 0 .../src/main/assets/media/emoji_flag.txt | 0 .../main/assets/media/emoji_food_drink.txt | 0 .../src/main/assets/media/emoji_objects.txt | 0 .../main/assets/media/emoji_people_body.txt | 0 .../assets/media/emoji_smileys_emotion.txt | 0 .../src/main/assets/media/emoji_symbols.txt | 0 .../main/assets/media/emoji_travel_places.txt | 0 .../libkeyboard/common/core/BaseKeyboard.kt | 0 .../common/core/BaseKeyboardIME.kt | 0 .../libkeyboard/common/core/IKeyboardIME.kt | 0 .../libkeyboard/common/ext/ContextExt.kt | 0 .../frogobox/libkeyboard/common/ext/IntExt.kt | 0 .../common/sound/MechanicalSoundManager.kt | 0 .../ui/emoji/AutoGridLayoutManager.kt | 0 .../libkeyboard/ui/emoji/EmojiCategory.kt | 0 .../libkeyboard/ui/emoji/EmojiCategoryType.kt | 0 .../libkeyboard/ui/emoji/EmojiHelper.kt | 0 .../libkeyboard/ui/emoji/EmojiKeyboard.kt | 181 +++++++++++ .../ui/emoji/EmojiKeyboardScreen.kt | 306 ++++++++++++++++++ .../libkeyboard/ui/main/ItemMainKeyboard.kt | 0 .../libkeyboard/ui/main/MainKeyboard.kt | 0 .../libkeyboard/ui/main/MainKeyboardScreen.kt | 179 ++++++++++ .../ui/main/OnKeyboardActionListener.kt | 0 .../ui/theme/FrogoLibKeyboardTheme.kt | 76 +++++ .../res/drawable/ic_keyboard_arrow_down.xml | 0 .../res/drawable/ic_keyboard_arrow_left.xml | 0 .../res/drawable/ic_keyboard_arrow_right.xml | 0 .../res/drawable/ic_keyboard_arrow_up.xml | 0 .../res/drawable/ic_keyboard_backspace.xml | 0 .../main/res/drawable/ic_keyboard_caps.xml | 0 .../res/drawable/ic_keyboard_caps_outline.xml | 0 .../drawable/ic_keyboard_caps_underlined.xml | 0 .../drawable/ic_keyboard_emoji_emotions.xml | 0 .../main/res/drawable/ic_keyboard_enter.xml | 0 .../main/res/drawable/ic_keyboard_search.xml | 0 .../main/res/drawable/ic_keyboard_send.xml | 0 .../src/main/res/drawable/ic_keyboard_tab.xml | 0 .../src/main/res/drawable/keypad_action.xml | 0 .../src/main/res/drawable/keypad_default.xml | 0 .../src/main/res/drawable/keypad_emoji.xml | 0 .../src/main/res/drawable/keypad_pop_up.xml | 0 .../main/res/layout/item_keyboard_emoji.xml | 0 .../main/res/layout/item_keyboard_main.xml | 0 .../src/main/res/layout/keyboard_emoji.xml | 0 .../main/res/layout/keyboard_main_mini.xml | 0 .../main/res/raw/mechanical_cherry_blue.wav | Bin .../main/res/raw/mechanical_cherry_brown.wav | Bin .../main/res/raw/mechanical_cherry_red.wav | Bin .../main/res/raw/mechanical_typewriter.wav | Bin .../src/main/res/values-night/colors.xml | 0 .../src/main/res/values-night/themes.xml | 0 .../src/main/res/values/attrs.xml | 0 .../src/main/res/values/colors.xml | 0 .../src/main/res/values/dimens.xml | 0 .../src/main/res/values/strings.xml | 1 + .../src/main/res/values/styles.xml | 0 .../src/main/res/values/themes.xml | 0 .../src/main/res/xml/keys_letter_persian.xml | 0 .../src/main/res/xml/keys_letters_bengali.xml | 0 .../main/res/xml/keys_letters_bulgarian.xml | 0 .../res/xml/keys_letters_english_dvorak.xml | 0 .../res/xml/keys_letters_english_qwerty.xml | 0 .../res/xml/keys_letters_english_qwertz.xml | 0 .../src/main/res/xml/keys_letters_french.xml | 0 .../src/main/res/xml/keys_letters_german.xml | 0 .../src/main/res/xml/keys_letters_greek.xml | 0 .../main/res/xml/keys_letters_lithuanian.xml | 0 .../main/res/xml/keys_letters_programmer.xml | 0 .../src/main/res/xml/keys_letters_qwerty.xml | 0 .../main/res/xml/keys_letters_romanian.xml | 0 .../src/main/res/xml/keys_letters_russian.xml | 0 .../main/res/xml/keys_letters_slovenian.xml | 0 .../res/xml/keys_letters_spanish_qwerty.xml | 0 .../main/res/xml/keys_letters_turkish_q.xml | 0 .../src/main/res/xml/keys_number.xml | 0 .../src/main/res/xml/keys_popup_template.xml | 0 .../src/main/res/xml/keys_symbols.xml | 0 .../src/main/res/xml/keys_symbols_shift.xml | 0 .../libkeyboard/BaseKeyboardIMETest.kt | 0 .../libkeyboard/EmojiKeyboardComposeTest.kt | 78 +++++ .../libkeyboard/KeyboardLayoutLogicTest.kt | 0 settings.gradle.kts | 2 +- 97 files changed, 857 insertions(+), 251 deletions(-) delete mode 100644 frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboard.kt rename {frogo-keyboard => lib-keyboard}/.gitignore (100%) rename {frogo-keyboard => lib-keyboard}/build.gradle.kts (73%) rename {frogo-keyboard => lib-keyboard}/consumer-rules.pro (100%) rename {frogo-keyboard => lib-keyboard}/proguard-rules.pro (100%) rename {frogo-keyboard => lib-keyboard}/src/main/AndroidManifest.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/assets/media/_emoji_general.txt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/assets/media/emoji_activities.txt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/assets/media/emoji_animal_nature.txt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/assets/media/emoji_flag.txt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/assets/media/emoji_food_drink.txt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/assets/media/emoji_objects.txt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/assets/media/emoji_people_body.txt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/assets/media/emoji_smileys_emotion.txt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/assets/media/emoji_symbols.txt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/assets/media/emoji_travel_places.txt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboard.kt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboardIME.kt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/java/com/frogobox/libkeyboard/common/core/IKeyboardIME.kt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/java/com/frogobox/libkeyboard/common/ext/ContextExt.kt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/java/com/frogobox/libkeyboard/common/ext/IntExt.kt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/java/com/frogobox/libkeyboard/common/sound/MechanicalSoundManager.kt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/java/com/frogobox/libkeyboard/ui/emoji/AutoGridLayoutManager.kt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiCategory.kt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiCategoryType.kt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiHelper.kt (100%) create mode 100644 lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboard.kt create mode 100644 lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboardScreen.kt rename {frogo-keyboard => lib-keyboard}/src/main/java/com/frogobox/libkeyboard/ui/main/ItemMainKeyboard.kt (100%) rename {frogo-keyboard => lib-keyboard}/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt (100%) create mode 100644 lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboardScreen.kt rename {frogo-keyboard => lib-keyboard}/src/main/java/com/frogobox/libkeyboard/ui/main/OnKeyboardActionListener.kt (100%) create mode 100644 lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/theme/FrogoLibKeyboardTheme.kt rename {frogo-keyboard => lib-keyboard}/src/main/res/drawable/ic_keyboard_arrow_down.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/drawable/ic_keyboard_arrow_left.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/drawable/ic_keyboard_arrow_right.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/drawable/ic_keyboard_arrow_up.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/drawable/ic_keyboard_backspace.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/drawable/ic_keyboard_caps.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/drawable/ic_keyboard_caps_outline.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/drawable/ic_keyboard_caps_underlined.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/drawable/ic_keyboard_emoji_emotions.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/drawable/ic_keyboard_enter.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/drawable/ic_keyboard_search.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/drawable/ic_keyboard_send.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/drawable/ic_keyboard_tab.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/drawable/keypad_action.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/drawable/keypad_default.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/drawable/keypad_emoji.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/drawable/keypad_pop_up.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/layout/item_keyboard_emoji.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/layout/item_keyboard_main.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/layout/keyboard_emoji.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/layout/keyboard_main_mini.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/raw/mechanical_cherry_blue.wav (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/raw/mechanical_cherry_brown.wav (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/raw/mechanical_cherry_red.wav (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/raw/mechanical_typewriter.wav (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/values-night/colors.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/values-night/themes.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/values/attrs.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/values/colors.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/values/dimens.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/values/strings.xml (97%) rename {frogo-keyboard => lib-keyboard}/src/main/res/values/styles.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/values/themes.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_letter_persian.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_letters_bengali.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_letters_bulgarian.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_letters_english_dvorak.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_letters_english_qwerty.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_letters_english_qwertz.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_letters_french.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_letters_german.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_letters_greek.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_letters_lithuanian.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_letters_programmer.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_letters_qwerty.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_letters_romanian.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_letters_russian.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_letters_slovenian.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_letters_spanish_qwerty.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_letters_turkish_q.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_number.xml (100%) mode change 100755 => 100644 rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_popup_template.xml (100%) rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_symbols.xml (100%) mode change 100755 => 100644 rename {frogo-keyboard => lib-keyboard}/src/main/res/xml/keys_symbols_shift.xml (100%) mode change 100755 => 100644 rename {frogo-keyboard => lib-keyboard}/src/test/java/com/frogobox/libkeyboard/BaseKeyboardIMETest.kt (100%) create mode 100644 lib-keyboard/src/test/java/com/frogobox/libkeyboard/EmojiKeyboardComposeTest.kt rename {frogo-keyboard => lib-keyboard}/src/test/java/com/frogobox/libkeyboard/KeyboardLayoutLogicTest.kt (100%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 844d883..ec5575f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -115,20 +115,7 @@ kotlin { dependencies { val composeBom = platform(libs.androidx.compose.bom) - implementation(composeBom) - androidTestImplementation(composeBom) - - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.compose.ui.graphics) - implementation(libs.androidx.compose.ui.tooling.preview) - debugImplementation(libs.androidx.compose.ui.tooling) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.material.icons.extended) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(libs.androidx.lifecycle.viewmodel.compose) - - implementation(project(DependencyGradle.MODULE_LIB_FROGO_KEYBOARD)) + implementation(project(DependencyGradle.MODULE_LIB_KEYBOARD)) implementation(libs.androidx.work.ktx) implementation(libs.google.hilt) @@ -137,11 +124,13 @@ dependencies { implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.ktx) - ksp(libs.github.glide.compiler) ksp(libs.google.hilt.compiler) ksp(libs.androidx.room.compiler) ksp(libs.androidx.lifecycle.compiler) + + androidTestImplementation(composeBom) testImplementation(libs.junit) - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0") + debugImplementation(libs.androidx.compose.ui.tooling) } \ No newline at end of file diff --git a/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt b/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt index 96858f0..e10d588 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt @@ -286,7 +286,7 @@ class KeyboardIME : BaseKeyboardIME() { keyboardWebview.gone() keyboardForm.gone() keyboardEmoji.gone() - keyboardEmoji.binding.emojiList.scrollToPosition(0) + keyboardEmoji.resetScroll() } } @@ -336,9 +336,9 @@ class KeyboardIME : BaseKeyboardIME() { showMainKeyboard() } - keyboardEmoji.binding.toolbarBack.setOnClickListener { + keyboardEmoji.setOnBackClickListener { keyboardEmoji.gone() - keyboardEmoji.binding.emojiList.scrollToPosition(0) + keyboardEmoji.resetScroll() showMainKeyboard() } diff --git a/buildSrc/src/main/kotlin/DependencyGradle.kt b/buildSrc/src/main/kotlin/DependencyGradle.kt index e0a6aad..efcddcc 100644 --- a/buildSrc/src/main/kotlin/DependencyGradle.kt +++ b/buildSrc/src/main/kotlin/DependencyGradle.kt @@ -1,5 +1,5 @@ object DependencyGradle { - const val MODULE_LIB_FROGO_KEYBOARD = ":frogo-keyboard" + const val MODULE_LIB_KEYBOARD = ":lib-keyboard" } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/ProjectSetting.kt b/buildSrc/src/main/kotlin/ProjectSetting.kt index 5e783c2..981f3b7 100644 --- a/buildSrc/src/main/kotlin/ProjectSetting.kt +++ b/buildSrc/src/main/kotlin/ProjectSetting.kt @@ -36,14 +36,14 @@ object ProjectSetting { const val BASE_PACKAGE_NAME = "com.frogobox" const val MODULE_APP = "appkeyboard" - const val MODULE_LIB_FROGO_KEYBOARD = "libkeyboard" + const val MODULE_LIB_KEYBOARD = "libkeyboard" val APP_NAME = NAME_APP.lowercase().replace(" ", "") val PROJECT_APP_ID = "$APP_DOMAIN.$APP_PLAY_CONSOLE.$APP_NAME" const val PROJECT_NAME_SPACE_APP = "$BASE_PACKAGE_NAME.$MODULE_APP" - const val PROJECT_NAME_SPACE_LIB_FROGO_KEYBOARD = "$BASE_PACKAGE_NAME.$MODULE_LIB_FROGO_KEYBOARD" + const val PROJECT_NAME_SPACE_LIB_FROGO_KEYBOARD = "$BASE_PACKAGE_NAME.$MODULE_LIB_KEYBOARD" const val PROJECT_VERSION_CODE = (VERSION_MAJOR * 100) + (VERSION_MINOR * 10) + (VERSION_PATCH * 1) const val PROJECT_VERSION_NAME = "$VERSION_MAJOR.$VERSION_MINOR.$VERSION_PATCH" diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboard.kt b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboard.kt deleted file mode 100644 index 354436a..0000000 --- a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboard.kt +++ /dev/null @@ -1,221 +0,0 @@ -package com.frogobox.libkeyboard.ui.emoji - -import android.content.Context -import android.graphics.Paint -import android.graphics.Typeface -import android.os.Handler -import android.os.Looper -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.LinearLayout -import androidx.emoji2.text.EmojiCompat -import com.frogobox.libkeyboard.R -import com.frogobox.libkeyboard.common.core.BaseKeyboard -import com.frogobox.libkeyboard.databinding.ItemKeyboardEmojiBinding -import com.frogobox.libkeyboard.databinding.KeyboardEmojiBinding -import com.frogobox.libkeyboard.ui.main.OnKeyboardActionListener -import com.frogobox.recycler.core.FrogoRecyclerNotifyListener -import com.frogobox.recycler.core.IFrogoBindingAdapter -import com.frogobox.recycler.ext.injectorBinding - -/** - * Created by Faisal Amir on 11/12/22 - * ----------------------------------------- - * E-mail : faisalamircs@gmail.com - * Github : github.com/amirisback - * ----------------------------------------- - * Copyright (C) Frogobox ID / amirisback - * All rights reserved - */ - -class EmojiKeyboard( - context: Context, - attrs: AttributeSet?, -) : BaseKeyboard(context, attrs) { - - private var emojiCompatMetadataVersion = 0 - - /** Cache parsed emoji lists to avoid re-parsing files on category switch */ - private val emojiCache = mutableMapOf>() - - /** Reusable Paint for glyph checking — avoid allocating every time */ - private val systemFontPaint = Paint().apply { - typeface = Typeface.DEFAULT - } - - var mOnKeyboardActionListener: OnKeyboardActionListener? = null - - override fun setupViewBinding(inflater: LayoutInflater, parent: LinearLayout): KeyboardEmojiBinding { - return KeyboardEmojiBinding.inflate(LayoutInflater.from(context), this, true) - } - - override fun onCreate() { - } - - fun openEmojiPalette() { - setupEmojis(EmojiCategoryType.GENERAL.path) - setupEmojiCategory() - } - - /** Clears the emoji cache. Call this if emoji assets are updated. */ - fun clearEmojiCache() { - emojiCache.clear() - } - - private fun setupEmojiCategory() { - val adapterCallback = - object : IFrogoBindingAdapter { - - override fun onItemClicked( - binding: ItemKeyboardEmojiBinding, - data: EmojiCategory, - position: Int, - notifyListener: FrogoRecyclerNotifyListener - ) { - setupEmojis(data.path) - } - - override fun areContentsTheSame( - oldItem: EmojiCategory, - newItem: EmojiCategory - ): Boolean { - return oldItem == newItem - } - - override fun areItemsTheSame( - oldItem: EmojiCategory, - newItem: EmojiCategory - ): Boolean { - return oldItem.name == newItem.name - } - - override fun setViewBinding(parent: ViewGroup): ItemKeyboardEmojiBinding { - return ItemKeyboardEmojiBinding.inflate( - LayoutInflater.from(context), - parent, - false - ) - } - - override fun setupInitComponent( - binding: ItemKeyboardEmojiBinding, - data: EmojiCategory, - position: Int, - notifyListener: FrogoRecyclerNotifyListener - ) { - val processed = EmojiCompat.get().process(data.icon) - binding.tvEmoji.text = processed - } - } - - binding.apply { - emojiCategoryList.injectorBinding() - .addData(getEmojiCategory()) - .addCallback(adapterCallback) - .createLayoutLinearHorizontal(false) - .build() - } - } - - private fun setupEmojis(path: String) { - // Check cache first - val cachedEmojis = emojiCache[path] - if (cachedEmojis != null) { - setupEmojiAdapter(cachedEmojis) - return - } - - ensureBackgroundThread { - val fullEmojiList = parseRawEmojiSpecsFile(context, path) - - val emojis = fullEmojiList.filter { emoji -> - systemFontPaint.hasGlyph(emoji) || (EmojiCompat.get().loadState == EmojiCompat.LOAD_STATE_SUCCEEDED - && EmojiCompat.get().getEmojiMatch( - emoji, - emojiCompatMetadataVersion - ) == EmojiCompat.EMOJI_SUPPORTED) - } - - // Store in cache - emojiCache[path] = emojis - - Handler(Looper.getMainLooper()).post { - setupEmojiAdapter(emojis) - } - } - } - - private fun setupEmojiAdapter(emojis: List) { - - val adapterCallback = object : IFrogoBindingAdapter { - override fun onItemClicked( - binding: ItemKeyboardEmojiBinding, - data: String, - position: Int, - notifyListener: FrogoRecyclerNotifyListener - ) { - mOnKeyboardActionListener?.onText(data) - } - - override fun onItemLongClicked( - binding: ItemKeyboardEmojiBinding, - data: String, - position: Int, - notifyListener: FrogoRecyclerNotifyListener - ) { - mOnKeyboardActionListener?.onText(data) - } - - override fun areContentsTheSame(oldItem: String, newItem: String): Boolean { - return oldItem == newItem - } - - override fun areItemsTheSame(oldItem: String, newItem: String): Boolean { - return oldItem == newItem - } - - override fun setViewBinding(parent: ViewGroup): ItemKeyboardEmojiBinding { - return ItemKeyboardEmojiBinding.inflate(LayoutInflater.from(context), parent, false) - } - - override fun setupInitComponent( - binding: ItemKeyboardEmojiBinding, - data: String, - position: Int, - notifyListener: FrogoRecyclerNotifyListener - ) { - val processed = EmojiCompat.get().process(data) - binding.tvEmoji.text = processed - } - } - - val emojiItemWidth = context.resources.getDimensionPixelSize(R.dimen.emoji_item_size) - val mLayoutManager = AutoGridLayoutManager(context, emojiItemWidth) - - binding.apply { - emojiList.injectorBinding() - .addCallback(adapterCallback) - .addData(emojis) - .build() - - emojiList.layoutManager = mLayoutManager - } - } - - private val backgroundExecutor = java.util.concurrent.Executors.newSingleThreadExecutor() - - override fun onDestroy() { - super.onDestroy() - backgroundExecutor.shutdown() - } - - private fun ensureBackgroundThread(callback: () -> Unit) { - if (Looper.myLooper() == Looper.getMainLooper()) { - backgroundExecutor.execute(callback) - } else { - callback() - } - } - -} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 5655886..f9138ec 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,11 +6,12 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1280m -XX:+UseParallelGC -XX:MaxMetaspaceSize=320m -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx4096m -XX:+UseParallelGC -XX:MaxMetaspaceSize=1024m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # Gradle Performance Flags org.gradle.daemon=true -org.gradle.parallel=false -org.gradle.workers.max=1 +org.gradle.daemon.idletimeout=180000 +org.gradle.parallel=true +org.gradle.workers.max=4 org.gradle.caching=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app"s APK @@ -21,8 +22,8 @@ android.enableJetifier=false # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official android.nonTransitiveRClass=true -ksp.incremental=false -kotlin.incremental=false +ksp.incremental=true +kotlin.incremental=true # Enabled parallel sync for Gradle 9.4+ org.gradle.tooling.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 354c539..4dad54f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,6 @@ liveCycle = "2.11.0" roomKtx = "2.8.5" work = "2.11.2" -glideCompiler = "5.0.9" hilt = "2.60.1" emoji2 = "1.6.0" @@ -41,7 +40,6 @@ google-hilt = { group = "com.google.dagger", name = "hilt-android", version.ref google-hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } # -------------------------------------------------------------------------------------------------- -github-glide-compiler = { group = "com.github.bumptech.glide", name = "compiler", version.ref = "glideCompiler" } frogo-android = { group = "com.github.frogobox", name = "frogo-sdk", version.ref = "frogoAndroid" } frogo-consume-api = { group = "com.github.frogobox", name = "frogo-consume-api", version.ref = "frogoconsumeapi" } # -------------------------------------------------------------------------------------------------- diff --git a/frogo-keyboard/.gitignore b/lib-keyboard/.gitignore similarity index 100% rename from frogo-keyboard/.gitignore rename to lib-keyboard/.gitignore diff --git a/frogo-keyboard/build.gradle.kts b/lib-keyboard/build.gradle.kts similarity index 73% rename from frogo-keyboard/build.gradle.kts rename to lib-keyboard/build.gradle.kts index 49cab6e..6353419 100644 --- a/frogo-keyboard/build.gradle.kts +++ b/lib-keyboard/build.gradle.kts @@ -2,6 +2,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.library) + alias(libs.plugins.jetbrains.kotlin.compose) `maven-publish` } @@ -32,6 +33,7 @@ android { buildFeatures { viewBinding = true buildConfig = true + compose = true } compileOptions { @@ -57,11 +59,27 @@ kotlin { dependencies { + val composeBom = platform(libs.androidx.compose.bom) + api(composeBom) + + api(libs.androidx.compose.ui) + api(libs.androidx.compose.ui.graphics) + api(libs.androidx.compose.ui.tooling.preview) + api(libs.androidx.compose.material3) + api(libs.androidx.compose.material.icons.extended) + + api(libs.androidx.activity.compose) + api(libs.androidx.lifecycle.runtime.ktx) + api(libs.androidx.lifecycle.viewmodel.compose) + api(libs.frogo.android) api(libs.frogo.consume.api) api(libs.androidx.emoji2) + androidTestImplementation(composeBom) testImplementation(libs.junit) + debugImplementation(libs.androidx.compose.ui.tooling) + } diff --git a/frogo-keyboard/consumer-rules.pro b/lib-keyboard/consumer-rules.pro similarity index 100% rename from frogo-keyboard/consumer-rules.pro rename to lib-keyboard/consumer-rules.pro diff --git a/frogo-keyboard/proguard-rules.pro b/lib-keyboard/proguard-rules.pro similarity index 100% rename from frogo-keyboard/proguard-rules.pro rename to lib-keyboard/proguard-rules.pro diff --git a/frogo-keyboard/src/main/AndroidManifest.xml b/lib-keyboard/src/main/AndroidManifest.xml similarity index 100% rename from frogo-keyboard/src/main/AndroidManifest.xml rename to lib-keyboard/src/main/AndroidManifest.xml diff --git a/frogo-keyboard/src/main/assets/media/_emoji_general.txt b/lib-keyboard/src/main/assets/media/_emoji_general.txt similarity index 100% rename from frogo-keyboard/src/main/assets/media/_emoji_general.txt rename to lib-keyboard/src/main/assets/media/_emoji_general.txt diff --git a/frogo-keyboard/src/main/assets/media/emoji_activities.txt b/lib-keyboard/src/main/assets/media/emoji_activities.txt similarity index 100% rename from frogo-keyboard/src/main/assets/media/emoji_activities.txt rename to lib-keyboard/src/main/assets/media/emoji_activities.txt diff --git a/frogo-keyboard/src/main/assets/media/emoji_animal_nature.txt b/lib-keyboard/src/main/assets/media/emoji_animal_nature.txt similarity index 100% rename from frogo-keyboard/src/main/assets/media/emoji_animal_nature.txt rename to lib-keyboard/src/main/assets/media/emoji_animal_nature.txt diff --git a/frogo-keyboard/src/main/assets/media/emoji_flag.txt b/lib-keyboard/src/main/assets/media/emoji_flag.txt similarity index 100% rename from frogo-keyboard/src/main/assets/media/emoji_flag.txt rename to lib-keyboard/src/main/assets/media/emoji_flag.txt diff --git a/frogo-keyboard/src/main/assets/media/emoji_food_drink.txt b/lib-keyboard/src/main/assets/media/emoji_food_drink.txt similarity index 100% rename from frogo-keyboard/src/main/assets/media/emoji_food_drink.txt rename to lib-keyboard/src/main/assets/media/emoji_food_drink.txt diff --git a/frogo-keyboard/src/main/assets/media/emoji_objects.txt b/lib-keyboard/src/main/assets/media/emoji_objects.txt similarity index 100% rename from frogo-keyboard/src/main/assets/media/emoji_objects.txt rename to lib-keyboard/src/main/assets/media/emoji_objects.txt diff --git a/frogo-keyboard/src/main/assets/media/emoji_people_body.txt b/lib-keyboard/src/main/assets/media/emoji_people_body.txt similarity index 100% rename from frogo-keyboard/src/main/assets/media/emoji_people_body.txt rename to lib-keyboard/src/main/assets/media/emoji_people_body.txt diff --git a/frogo-keyboard/src/main/assets/media/emoji_smileys_emotion.txt b/lib-keyboard/src/main/assets/media/emoji_smileys_emotion.txt similarity index 100% rename from frogo-keyboard/src/main/assets/media/emoji_smileys_emotion.txt rename to lib-keyboard/src/main/assets/media/emoji_smileys_emotion.txt diff --git a/frogo-keyboard/src/main/assets/media/emoji_symbols.txt b/lib-keyboard/src/main/assets/media/emoji_symbols.txt similarity index 100% rename from frogo-keyboard/src/main/assets/media/emoji_symbols.txt rename to lib-keyboard/src/main/assets/media/emoji_symbols.txt diff --git a/frogo-keyboard/src/main/assets/media/emoji_travel_places.txt b/lib-keyboard/src/main/assets/media/emoji_travel_places.txt similarity index 100% rename from frogo-keyboard/src/main/assets/media/emoji_travel_places.txt rename to lib-keyboard/src/main/assets/media/emoji_travel_places.txt diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboard.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboard.kt similarity index 100% rename from frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboard.kt rename to lib-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboard.kt diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboardIME.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboardIME.kt similarity index 100% rename from frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboardIME.kt rename to lib-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboardIME.kt diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/IKeyboardIME.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/IKeyboardIME.kt similarity index 100% rename from frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/IKeyboardIME.kt rename to lib-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/IKeyboardIME.kt diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/ext/ContextExt.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/common/ext/ContextExt.kt similarity index 100% rename from frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/ext/ContextExt.kt rename to lib-keyboard/src/main/java/com/frogobox/libkeyboard/common/ext/ContextExt.kt diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/ext/IntExt.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/common/ext/IntExt.kt similarity index 100% rename from frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/ext/IntExt.kt rename to lib-keyboard/src/main/java/com/frogobox/libkeyboard/common/ext/IntExt.kt diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/sound/MechanicalSoundManager.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/common/sound/MechanicalSoundManager.kt similarity index 100% rename from frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/sound/MechanicalSoundManager.kt rename to lib-keyboard/src/main/java/com/frogobox/libkeyboard/common/sound/MechanicalSoundManager.kt diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/AutoGridLayoutManager.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/AutoGridLayoutManager.kt similarity index 100% rename from frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/AutoGridLayoutManager.kt rename to lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/AutoGridLayoutManager.kt diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiCategory.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiCategory.kt similarity index 100% rename from frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiCategory.kt rename to lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiCategory.kt diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiCategoryType.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiCategoryType.kt similarity index 100% rename from frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiCategoryType.kt rename to lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiCategoryType.kt diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiHelper.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiHelper.kt similarity index 100% rename from frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiHelper.kt rename to lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiHelper.kt diff --git a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboard.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboard.kt new file mode 100644 index 0000000..7de1814 --- /dev/null +++ b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboard.kt @@ -0,0 +1,181 @@ +package com.frogobox.libkeyboard.ui.emoji + +import android.content.Context +import android.graphics.Paint +import android.graphics.Typeface +import android.util.AttributeSet +import android.view.inputmethod.InputConnection +import android.widget.FrameLayout +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.emoji2.text.EmojiCompat +import com.frogobox.libkeyboard.ui.main.OnKeyboardActionListener +import com.frogobox.libkeyboard.ui.theme.FrogoLibKeyboardTheme +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Modern Jetpack Compose-based Emoji Keyboard panel. + * + * Hosts [EmojiKeyboardScreen] within a ComposeView, supporting + * responsive emoji grids, category tabs, and input connection commits. + */ +class EmojiKeyboard @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { + + private val job = SupervisorJob() + private val scope = CoroutineScope(Dispatchers.Main + job) + + private val emojiCache = mutableMapOf>() + private val systemFontPaint = Paint().apply { + typeface = Typeface.DEFAULT + } + private var emojiCompatMetadataVersion = 0 + + private val emojisState = MutableStateFlow>(emptyList()) + private val selectedCategoryState = MutableStateFlow(EmojiCategoryType.GENERAL) + private val isLoadingState = MutableStateFlow(false) + private var resetScrollTrigger by mutableIntStateOf(0) + + var currentInputConnection: InputConnection? = null + var mOnKeyboardActionListener: OnKeyboardActionListener? = null + var onBackClick: (() -> Unit)? = null + + private val composeView = ComposeView(context).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool) + setContent { + FrogoLibKeyboardTheme { + val emojis by emojisState.collectAsState() + val selectedCategory by selectedCategoryState.collectAsState() + val isLoading by isLoadingState.collectAsState() + val gridState = rememberLazyGridState() + + LaunchedEffect(resetScrollTrigger) { + if (resetScrollTrigger > 0) { + gridState.scrollToItem(0) + } + } + + EmojiKeyboardScreen( + emojis = emojis, + selectedCategory = selectedCategory, + categories = getEmojiCategory(), + isLoading = isLoading, + lazyGridState = gridState, + onCategorySelected = { category -> + selectedCategoryState.value = category + loadEmojis(category.path) + resetScroll() + }, + onEmojiClicked = { emoji -> + currentInputConnection?.commitText(emoji, 1) + mOnKeyboardActionListener?.onText(emoji) + mOnKeyboardActionListener?.onActionUp() + }, + onBackClicked = { + onBackClick?.invoke() + } + ) + } + } + } + + init { + addView(composeView) + openEmojiPalette() + } + + /** + * Assigns the active [InputConnection] for committing typed emojis. + */ + fun setInputConnection(inputConnection: InputConnection?) { + currentInputConnection = inputConnection + } + + /** + * Registers a listener for back navigation clicks from the emoji header. + */ + fun setOnBackClickListener(listener: () -> Unit) { + onBackClick = listener + } + + /** + * Opens the emoji palette at the default category. + */ + fun openEmojiPalette() { + selectedCategoryState.value = EmojiCategoryType.GENERAL + loadEmojis(EmojiCategoryType.GENERAL.path) + } + + /** + * Clears cached emoji lists from memory. + */ + fun clearEmojiCache() { + emojiCache.clear() + } + + /** + * Smoothly or instantly scrolls the emoji grid back to item 0. + */ + fun resetScroll() { + resetScrollTrigger++ + } + + private fun loadEmojis(path: String) { + val cached = emojiCache[path] + if (cached != null) { + emojisState.value = cached + return + } + + isLoadingState.value = true + scope.launch(Dispatchers.IO) { + val fullEmojiList = parseRawEmojiSpecsFile(context, path) + + val isEmojiCompatReady = EmojiCompat.isConfigured() && + EmojiCompat.get().loadState == EmojiCompat.LOAD_STATE_SUCCEEDED + + val emojis = fullEmojiList.filter { emoji -> + systemFontPaint.hasGlyph(emoji) || (isEmojiCompatReady && + EmojiCompat.get().getEmojiMatch( + emoji, + emojiCompatMetadataVersion + ) == EmojiCompat.EMOJI_SUPPORTED) + } + + emojiCache[path] = emojis + + withContext(Dispatchers.Main) { + emojisState.value = emojis + isLoadingState.value = false + } + } + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + job.cancelChildren() + currentInputConnection = null + } + + fun onDestroy() { + job.cancelChildren() + currentInputConnection = null + clearEmojiCache() + } + +} \ No newline at end of file diff --git a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboardScreen.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboardScreen.kt new file mode 100644 index 0000000..e80bb12 --- /dev/null +++ b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboardScreen.kt @@ -0,0 +1,306 @@ +package com.frogobox.libkeyboard.ui.emoji + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.emoji2.text.EmojiCompat +import com.frogobox.libkeyboard.R +import com.frogobox.libkeyboard.ui.theme.FrogoLibKeyboardTheme + +val EmojiCategoryType.displayName: String + get() = when (this) { + EmojiCategoryType.GENERAL -> "General" + EmojiCategoryType.ACTIVITIES -> "Activities" + EmojiCategoryType.ANIMAL_NATURE -> "Animals & Nature" + EmojiCategoryType.FLAG -> "Flags" + EmojiCategoryType.FOOD_DRINK -> "Food & Drink" + EmojiCategoryType.OBJECTS -> "Objects" + EmojiCategoryType.PEOPLE_BODY -> "People & Body" + EmojiCategoryType.SMILEYS_EMOTION -> "Smileys & Emotion" + EmojiCategoryType.SYMBOLS -> "Symbols" + EmojiCategoryType.TRAVEL_PLACES -> "Travel & Places" + } + +/** + * Modern Jetpack Compose Screen for browsing and selecting emojis. + */ +@Composable +fun EmojiKeyboardScreen( + emojis: List, + selectedCategory: EmojiCategoryType, + categories: List, + onCategorySelected: (EmojiCategoryType) -> Unit, + onEmojiClicked: (String) -> Unit, + onBackClicked: () -> Unit, + modifier: Modifier = Modifier, + lazyGridState: LazyGridState = rememberLazyGridState(), + isLoading: Boolean = false +) { + Surface( + modifier = modifier + .fillMaxWidth() + .height(260.dp), + color = MaterialTheme.colorScheme.surface + ) { + Column(modifier = Modifier.fillMaxSize()) { + // Header Toolbar + EmojiHeaderToolbar( + selectedCategory = selectedCategory, + onBackClicked = onBackClicked + ) + + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + thickness = 1.dp, + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) + ) + + // Emoji Grid or Loading State + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(36.dp), + color = MaterialTheme.colorScheme.primary, + strokeWidth = 3.dp + ) + } else if (emojis.isEmpty()) { + Text( + text = "No emojis found", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } else { + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 42.dp), + state = lazyGridState, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 4.dp, vertical = 4.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp) + ) { + items(emojis, key = { it }) { emoji -> + EmojiGridItem( + emoji = emoji, + onClick = { onEmojiClicked(emoji) } + ) + } + } + } + } + + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + thickness = 1.dp, + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) + ) + + // Category Selector Bar + EmojiCategoryBar( + selectedCategory = selectedCategory, + categories = categories, + onCategorySelected = onCategorySelected + ) + } + } +} + +@Composable +fun EmojiHeaderToolbar( + selectedCategory: EmojiCategoryType, + onBackClicked: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .height(48.dp) + .padding(horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + onClick = onBackClicked, + modifier = Modifier.size(40.dp) + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(id = R.string.back), + tint = MaterialTheme.colorScheme.onSurface + ) + } + + Column( + modifier = Modifier + .weight(1f) + .padding(start = 4.dp) + ) { + Text( + text = stringResource(id = R.string.emojis), + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = selectedCategory.displayName, + fontSize = 11.sp, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@Composable +fun EmojiGridItem( + emoji: String, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val displayEmoji = remember(emoji) { + if (EmojiCompat.isConfigured()) { + try { + EmojiCompat.get().process(emoji).toString() + } catch (e: Exception) { + emoji + } + } else { + emoji + } + } + + Box( + modifier = modifier + .size(42.dp) + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onClick), + contentAlignment = Alignment.Center + ) { + Text( + text = displayEmoji, + fontSize = 24.sp, + color = MaterialTheme.colorScheme.onSurface + ) + } +} + +@Composable +fun EmojiCategoryBar( + selectedCategory: EmojiCategoryType, + categories: List, + onCategorySelected: (EmojiCategoryType) -> Unit, + modifier: Modifier = Modifier +) { + LazyRow( + modifier = modifier + .fillMaxWidth() + .height(44.dp) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), + horizontalArrangement = Arrangement.spacedBy(4.dp), + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + items(categories) { category -> + val type = EmojiCategoryType.entries.firstOrNull { it.name == category.name } ?: EmojiCategoryType.GENERAL + val isSelected = type == selectedCategory + + Surface( + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { onCategorySelected(type) }, + shape = RoundedCornerShape(16.dp), + color = if (isSelected) { + MaterialTheme.colorScheme.primaryContainer + } else { + Color.Transparent + } + ) { + Box( + modifier = Modifier + .padding(horizontal = 10.dp, vertical = 6.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = category.icon, + fontSize = 18.sp + ) + } + } + } + } +} + +@Preview(name = "Light Mode", showBackground = true) +@Composable +private fun EmojiKeyboardScreenPreviewLight() { + FrogoLibKeyboardTheme(darkTheme = false) { + EmojiKeyboardScreen( + emojis = listOf("😀", "😃", "😄", "😁", "😆", "😅", "😂", "🤣", "😊", "😇", "🙂", "🙃", "😉", "😍"), + selectedCategory = EmojiCategoryType.SMILEYS_EMOTION, + categories = getEmojiCategory(), + onCategorySelected = {}, + onEmojiClicked = {}, + onBackClicked = {} + ) + } +} + +@Preview(name = "Dark Mode", showBackground = true) +@Composable +private fun EmojiKeyboardScreenPreviewDark() { + FrogoLibKeyboardTheme(darkTheme = true) { + EmojiKeyboardScreen( + emojis = listOf("😀", "😃", "😄", "😁", "😆", "😅", "😂", "🤣", "😊", "😇", "🙂", "🙃", "😉", "😍"), + selectedCategory = EmojiCategoryType.SMILEYS_EMOTION, + categories = getEmojiCategory(), + onCategorySelected = {}, + onEmojiClicked = {}, + onBackClicked = {} + ) + } +} diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/ItemMainKeyboard.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/ItemMainKeyboard.kt similarity index 100% rename from frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/ItemMainKeyboard.kt rename to lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/ItemMainKeyboard.kt diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt similarity index 100% rename from frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt rename to lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt diff --git a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboardScreen.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboardScreen.kt new file mode 100644 index 0000000..032a9fa --- /dev/null +++ b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboardScreen.kt @@ -0,0 +1,179 @@ +package com.frogobox.libkeyboard.ui.main + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import com.frogobox.libkeyboard.ui.theme.FrogoLibKeyboardTheme +import com.frogobox.libkeyboard.ui.theme.KeypadActionDark +import com.frogobox.libkeyboard.ui.theme.KeypadActionLight +import com.frogobox.libkeyboard.ui.theme.KeypadDark +import com.frogobox.libkeyboard.ui.theme.KeypadLight + +/** + * Jetpack Compose interoperability wrapper for hosting [MainKeyboard] within Compose UI trees. + */ +@Composable +fun MainKeyboardView( + keyboard: ItemMainKeyboard?, + onActionListener: OnKeyboardActionListener?, + modifier: Modifier = Modifier, + onInit: (MainKeyboard) -> Unit = {} +) { + AndroidView( + modifier = modifier, + factory = { context -> + MainKeyboard(context, null).apply { + this.mOnKeyboardActionListener = onActionListener + keyboard?.let { setKeyboard(it) } + onInit(this) + } + }, + update = { view -> + view.mOnKeyboardActionListener = onActionListener + if (keyboard != null) { + view.setKeyboard(keyboard) + } + } + ) +} + +/** + * Declarative Jetpack Compose component rendering keyboard keys based on [ItemMainKeyboard]. + */ +@Composable +fun MainKeyboardComposable( + keyboard: ItemMainKeyboard, + onKeyPress: (Int) -> Unit, + modifier: Modifier = Modifier, + onKeyActionUp: () -> Unit = {}, + isDarkTheme: Boolean = false +) { + val defaultKeyColor = if (isDarkTheme) KeypadDark else KeypadLight + val actionKeyColor = if (isDarkTheme) KeypadActionDark else KeypadActionLight + + Column( + modifier = modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(horizontal = 4.dp, vertical = 6.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + // Group keys by their approximate row position + val keys = keyboard.mKeys + if (keys.isNotEmpty()) { + val rowYPositions = keys.map { it.y }.distinct().sorted() + + for (rowY in rowYPositions) { + val rowKeys = keys.filter { it.y == rowY }.sortedBy { it.x } + val totalRowWidth = rowKeys.sumOf { it.width }.coerceAtLeast(1) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(3.dp), + verticalAlignment = Alignment.CenterVertically + ) { + for (key in rowKeys) { + val weight = (key.width.toFloat() / totalRowWidth.toFloat()).coerceAtLeast(0.01f) + val isActionKey = when (key.code) { + ItemMainKeyboard.KEYCODE_SHIFT, + ItemMainKeyboard.KEYCODE_DELETE, + ItemMainKeyboard.KEYCODE_ENTER, + ItemMainKeyboard.KEYCODE_MODE_CHANGE, + ItemMainKeyboard.KEYCODE_TAB -> true + else -> false + } + + val keyBackground = if (isActionKey) actionKeyColor else defaultKeyColor + + KeyItemView( + label = key.label.toString(), + code = key.code, + weight = weight, + backgroundColor = keyBackground, + isActionKey = isActionKey, + onClick = { + onKeyPress(key.code) + onKeyActionUp() + }, + modifier = Modifier.weight(weight) + ) + } + } + } + } + } +} + +@Composable +private fun KeyItemView( + label: String, + code: Int, + weight: Float, + backgroundColor: Color, + isActionKey: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val interactionSource = remember { MutableInteractionSource() } + + Surface( + modifier = modifier + .height(46.dp) + .clip(RoundedCornerShape(6.dp)) + .clickable( + interactionSource = interactionSource, + indication = ripple(bounded = true), + onClick = onClick + ), + shape = RoundedCornerShape(6.dp), + color = backgroundColor, + shadowElevation = if (isActionKey) 1.dp else 1.5.dp + ) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + val displayLabel = when (code) { + ItemMainKeyboard.KEYCODE_SHIFT -> "⇧" + ItemMainKeyboard.KEYCODE_DELETE -> "⌫" + ItemMainKeyboard.KEYCODE_ENTER -> "↵" + ItemMainKeyboard.KEYCODE_SPACE -> " " + ItemMainKeyboard.KEYCODE_MODE_CHANGE -> "?123" + ItemMainKeyboard.KEYCODE_EMOJI -> "🙂" + ItemMainKeyboard.KEYCODE_TAB -> "⇥" + else -> label + } + + Text( + text = displayLabel, + fontSize = if (displayLabel.length > 2) 13.sp else 18.sp, + fontWeight = if (displayLabel.length > 2 || isActionKey) FontWeight.SemiBold else FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center + ) + } + } +} diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/OnKeyboardActionListener.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/OnKeyboardActionListener.kt similarity index 100% rename from frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/OnKeyboardActionListener.kt rename to lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/OnKeyboardActionListener.kt diff --git a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/theme/FrogoLibKeyboardTheme.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/theme/FrogoLibKeyboardTheme.kt new file mode 100644 index 0000000..6e1d302 --- /dev/null +++ b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/theme/FrogoLibKeyboardTheme.kt @@ -0,0 +1,76 @@ +package com.frogobox.libkeyboard.ui.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext + +val KeyboardTealPrimary = Color(0xFF009688) +val KeyboardTealPrimaryDark = Color(0xFF80CBC4) +val KeyboardTealPrimaryContainer = Color(0xFFB2DFDB) +val KeyboardTealPrimaryContainerDark = Color(0xFF004D40) + +val KeyboardSurfaceLight = Color(0xFFFFFFFF) +val KeyboardSurfaceDark = Color(0xFF1E1E1E) +val KeyboardBackgroundLight = Color(0xFFF5F5F5) +val KeyboardBackgroundDark = Color(0xFF121212) + +val KeypadLight = Color(0xFFE3E5E8) +val KeypadDark = Color(0xFF2C2C2C) +val KeypadActionLight = Color(0xFFCFD4D9) +val KeypadActionDark = Color(0xFF3A3A3A) + +private val DarkColorScheme = darkColorScheme( + primary = KeyboardTealPrimaryDark, + onPrimary = Color(0xFF003731), + primaryContainer = KeyboardTealPrimaryContainerDark, + onPrimaryContainer = Color(0xFFB2DFDB), + surface = KeyboardSurfaceDark, + onSurface = Color(0xFFE0E0E0), + surfaceVariant = Color(0xFF2A2A2A), + onSurfaceVariant = Color(0xFFB0B0B0), + background = KeyboardBackgroundDark, + onBackground = Color(0xFFE0E0E0), + outline = Color(0xFF383838) +) + +private val LightColorScheme = lightColorScheme( + primary = KeyboardTealPrimary, + onPrimary = Color.White, + primaryContainer = KeyboardTealPrimaryContainer, + onPrimaryContainer = Color(0xFF004D40), + surface = KeyboardSurfaceLight, + onSurface = Color(0xFF212121), + surfaceVariant = Color(0xFFF0F0F0), + onSurfaceVariant = Color(0xFF616161), + background = KeyboardBackgroundLight, + onBackground = Color(0xFF212121), + outline = Color(0xFFE0E0E0) +) + +@Composable +fun FrogoLibKeyboardTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + content = content + ) +} diff --git a/frogo-keyboard/src/main/res/drawable/ic_keyboard_arrow_down.xml b/lib-keyboard/src/main/res/drawable/ic_keyboard_arrow_down.xml similarity index 100% rename from frogo-keyboard/src/main/res/drawable/ic_keyboard_arrow_down.xml rename to lib-keyboard/src/main/res/drawable/ic_keyboard_arrow_down.xml diff --git a/frogo-keyboard/src/main/res/drawable/ic_keyboard_arrow_left.xml b/lib-keyboard/src/main/res/drawable/ic_keyboard_arrow_left.xml similarity index 100% rename from frogo-keyboard/src/main/res/drawable/ic_keyboard_arrow_left.xml rename to lib-keyboard/src/main/res/drawable/ic_keyboard_arrow_left.xml diff --git a/frogo-keyboard/src/main/res/drawable/ic_keyboard_arrow_right.xml b/lib-keyboard/src/main/res/drawable/ic_keyboard_arrow_right.xml similarity index 100% rename from frogo-keyboard/src/main/res/drawable/ic_keyboard_arrow_right.xml rename to lib-keyboard/src/main/res/drawable/ic_keyboard_arrow_right.xml diff --git a/frogo-keyboard/src/main/res/drawable/ic_keyboard_arrow_up.xml b/lib-keyboard/src/main/res/drawable/ic_keyboard_arrow_up.xml similarity index 100% rename from frogo-keyboard/src/main/res/drawable/ic_keyboard_arrow_up.xml rename to lib-keyboard/src/main/res/drawable/ic_keyboard_arrow_up.xml diff --git a/frogo-keyboard/src/main/res/drawable/ic_keyboard_backspace.xml b/lib-keyboard/src/main/res/drawable/ic_keyboard_backspace.xml similarity index 100% rename from frogo-keyboard/src/main/res/drawable/ic_keyboard_backspace.xml rename to lib-keyboard/src/main/res/drawable/ic_keyboard_backspace.xml diff --git a/frogo-keyboard/src/main/res/drawable/ic_keyboard_caps.xml b/lib-keyboard/src/main/res/drawable/ic_keyboard_caps.xml similarity index 100% rename from frogo-keyboard/src/main/res/drawable/ic_keyboard_caps.xml rename to lib-keyboard/src/main/res/drawable/ic_keyboard_caps.xml diff --git a/frogo-keyboard/src/main/res/drawable/ic_keyboard_caps_outline.xml b/lib-keyboard/src/main/res/drawable/ic_keyboard_caps_outline.xml similarity index 100% rename from frogo-keyboard/src/main/res/drawable/ic_keyboard_caps_outline.xml rename to lib-keyboard/src/main/res/drawable/ic_keyboard_caps_outline.xml diff --git a/frogo-keyboard/src/main/res/drawable/ic_keyboard_caps_underlined.xml b/lib-keyboard/src/main/res/drawable/ic_keyboard_caps_underlined.xml similarity index 100% rename from frogo-keyboard/src/main/res/drawable/ic_keyboard_caps_underlined.xml rename to lib-keyboard/src/main/res/drawable/ic_keyboard_caps_underlined.xml diff --git a/frogo-keyboard/src/main/res/drawable/ic_keyboard_emoji_emotions.xml b/lib-keyboard/src/main/res/drawable/ic_keyboard_emoji_emotions.xml similarity index 100% rename from frogo-keyboard/src/main/res/drawable/ic_keyboard_emoji_emotions.xml rename to lib-keyboard/src/main/res/drawable/ic_keyboard_emoji_emotions.xml diff --git a/frogo-keyboard/src/main/res/drawable/ic_keyboard_enter.xml b/lib-keyboard/src/main/res/drawable/ic_keyboard_enter.xml similarity index 100% rename from frogo-keyboard/src/main/res/drawable/ic_keyboard_enter.xml rename to lib-keyboard/src/main/res/drawable/ic_keyboard_enter.xml diff --git a/frogo-keyboard/src/main/res/drawable/ic_keyboard_search.xml b/lib-keyboard/src/main/res/drawable/ic_keyboard_search.xml similarity index 100% rename from frogo-keyboard/src/main/res/drawable/ic_keyboard_search.xml rename to lib-keyboard/src/main/res/drawable/ic_keyboard_search.xml diff --git a/frogo-keyboard/src/main/res/drawable/ic_keyboard_send.xml b/lib-keyboard/src/main/res/drawable/ic_keyboard_send.xml similarity index 100% rename from frogo-keyboard/src/main/res/drawable/ic_keyboard_send.xml rename to lib-keyboard/src/main/res/drawable/ic_keyboard_send.xml diff --git a/frogo-keyboard/src/main/res/drawable/ic_keyboard_tab.xml b/lib-keyboard/src/main/res/drawable/ic_keyboard_tab.xml similarity index 100% rename from frogo-keyboard/src/main/res/drawable/ic_keyboard_tab.xml rename to lib-keyboard/src/main/res/drawable/ic_keyboard_tab.xml diff --git a/frogo-keyboard/src/main/res/drawable/keypad_action.xml b/lib-keyboard/src/main/res/drawable/keypad_action.xml similarity index 100% rename from frogo-keyboard/src/main/res/drawable/keypad_action.xml rename to lib-keyboard/src/main/res/drawable/keypad_action.xml diff --git a/frogo-keyboard/src/main/res/drawable/keypad_default.xml b/lib-keyboard/src/main/res/drawable/keypad_default.xml similarity index 100% rename from frogo-keyboard/src/main/res/drawable/keypad_default.xml rename to lib-keyboard/src/main/res/drawable/keypad_default.xml diff --git a/frogo-keyboard/src/main/res/drawable/keypad_emoji.xml b/lib-keyboard/src/main/res/drawable/keypad_emoji.xml similarity index 100% rename from frogo-keyboard/src/main/res/drawable/keypad_emoji.xml rename to lib-keyboard/src/main/res/drawable/keypad_emoji.xml diff --git a/frogo-keyboard/src/main/res/drawable/keypad_pop_up.xml b/lib-keyboard/src/main/res/drawable/keypad_pop_up.xml similarity index 100% rename from frogo-keyboard/src/main/res/drawable/keypad_pop_up.xml rename to lib-keyboard/src/main/res/drawable/keypad_pop_up.xml diff --git a/frogo-keyboard/src/main/res/layout/item_keyboard_emoji.xml b/lib-keyboard/src/main/res/layout/item_keyboard_emoji.xml similarity index 100% rename from frogo-keyboard/src/main/res/layout/item_keyboard_emoji.xml rename to lib-keyboard/src/main/res/layout/item_keyboard_emoji.xml diff --git a/frogo-keyboard/src/main/res/layout/item_keyboard_main.xml b/lib-keyboard/src/main/res/layout/item_keyboard_main.xml similarity index 100% rename from frogo-keyboard/src/main/res/layout/item_keyboard_main.xml rename to lib-keyboard/src/main/res/layout/item_keyboard_main.xml diff --git a/frogo-keyboard/src/main/res/layout/keyboard_emoji.xml b/lib-keyboard/src/main/res/layout/keyboard_emoji.xml similarity index 100% rename from frogo-keyboard/src/main/res/layout/keyboard_emoji.xml rename to lib-keyboard/src/main/res/layout/keyboard_emoji.xml diff --git a/frogo-keyboard/src/main/res/layout/keyboard_main_mini.xml b/lib-keyboard/src/main/res/layout/keyboard_main_mini.xml similarity index 100% rename from frogo-keyboard/src/main/res/layout/keyboard_main_mini.xml rename to lib-keyboard/src/main/res/layout/keyboard_main_mini.xml diff --git a/frogo-keyboard/src/main/res/raw/mechanical_cherry_blue.wav b/lib-keyboard/src/main/res/raw/mechanical_cherry_blue.wav similarity index 100% rename from frogo-keyboard/src/main/res/raw/mechanical_cherry_blue.wav rename to lib-keyboard/src/main/res/raw/mechanical_cherry_blue.wav diff --git a/frogo-keyboard/src/main/res/raw/mechanical_cherry_brown.wav b/lib-keyboard/src/main/res/raw/mechanical_cherry_brown.wav similarity index 100% rename from frogo-keyboard/src/main/res/raw/mechanical_cherry_brown.wav rename to lib-keyboard/src/main/res/raw/mechanical_cherry_brown.wav diff --git a/frogo-keyboard/src/main/res/raw/mechanical_cherry_red.wav b/lib-keyboard/src/main/res/raw/mechanical_cherry_red.wav similarity index 100% rename from frogo-keyboard/src/main/res/raw/mechanical_cherry_red.wav rename to lib-keyboard/src/main/res/raw/mechanical_cherry_red.wav diff --git a/frogo-keyboard/src/main/res/raw/mechanical_typewriter.wav b/lib-keyboard/src/main/res/raw/mechanical_typewriter.wav similarity index 100% rename from frogo-keyboard/src/main/res/raw/mechanical_typewriter.wav rename to lib-keyboard/src/main/res/raw/mechanical_typewriter.wav diff --git a/frogo-keyboard/src/main/res/values-night/colors.xml b/lib-keyboard/src/main/res/values-night/colors.xml similarity index 100% rename from frogo-keyboard/src/main/res/values-night/colors.xml rename to lib-keyboard/src/main/res/values-night/colors.xml diff --git a/frogo-keyboard/src/main/res/values-night/themes.xml b/lib-keyboard/src/main/res/values-night/themes.xml similarity index 100% rename from frogo-keyboard/src/main/res/values-night/themes.xml rename to lib-keyboard/src/main/res/values-night/themes.xml diff --git a/frogo-keyboard/src/main/res/values/attrs.xml b/lib-keyboard/src/main/res/values/attrs.xml similarity index 100% rename from frogo-keyboard/src/main/res/values/attrs.xml rename to lib-keyboard/src/main/res/values/attrs.xml diff --git a/frogo-keyboard/src/main/res/values/colors.xml b/lib-keyboard/src/main/res/values/colors.xml similarity index 100% rename from frogo-keyboard/src/main/res/values/colors.xml rename to lib-keyboard/src/main/res/values/colors.xml diff --git a/frogo-keyboard/src/main/res/values/dimens.xml b/lib-keyboard/src/main/res/values/dimens.xml similarity index 100% rename from frogo-keyboard/src/main/res/values/dimens.xml rename to lib-keyboard/src/main/res/values/dimens.xml diff --git a/frogo-keyboard/src/main/res/values/strings.xml b/lib-keyboard/src/main/res/values/strings.xml similarity index 97% rename from frogo-keyboard/src/main/res/values/strings.xml rename to lib-keyboard/src/main/res/values/strings.xml index 5f03446..a22c765 100644 --- a/frogo-keyboard/src/main/res/values/strings.xml +++ b/lib-keyboard/src/main/res/values/strings.xml @@ -6,6 +6,7 @@ Shift Enter + Back Emojis Welcome to Frogo Keyboard diff --git a/frogo-keyboard/src/main/res/values/styles.xml b/lib-keyboard/src/main/res/values/styles.xml similarity index 100% rename from frogo-keyboard/src/main/res/values/styles.xml rename to lib-keyboard/src/main/res/values/styles.xml diff --git a/frogo-keyboard/src/main/res/values/themes.xml b/lib-keyboard/src/main/res/values/themes.xml similarity index 100% rename from frogo-keyboard/src/main/res/values/themes.xml rename to lib-keyboard/src/main/res/values/themes.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_letter_persian.xml b/lib-keyboard/src/main/res/xml/keys_letter_persian.xml similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_letter_persian.xml rename to lib-keyboard/src/main/res/xml/keys_letter_persian.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_letters_bengali.xml b/lib-keyboard/src/main/res/xml/keys_letters_bengali.xml similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_letters_bengali.xml rename to lib-keyboard/src/main/res/xml/keys_letters_bengali.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_letters_bulgarian.xml b/lib-keyboard/src/main/res/xml/keys_letters_bulgarian.xml similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_letters_bulgarian.xml rename to lib-keyboard/src/main/res/xml/keys_letters_bulgarian.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_letters_english_dvorak.xml b/lib-keyboard/src/main/res/xml/keys_letters_english_dvorak.xml similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_letters_english_dvorak.xml rename to lib-keyboard/src/main/res/xml/keys_letters_english_dvorak.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_letters_english_qwerty.xml b/lib-keyboard/src/main/res/xml/keys_letters_english_qwerty.xml similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_letters_english_qwerty.xml rename to lib-keyboard/src/main/res/xml/keys_letters_english_qwerty.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_letters_english_qwertz.xml b/lib-keyboard/src/main/res/xml/keys_letters_english_qwertz.xml similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_letters_english_qwertz.xml rename to lib-keyboard/src/main/res/xml/keys_letters_english_qwertz.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_letters_french.xml b/lib-keyboard/src/main/res/xml/keys_letters_french.xml similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_letters_french.xml rename to lib-keyboard/src/main/res/xml/keys_letters_french.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_letters_german.xml b/lib-keyboard/src/main/res/xml/keys_letters_german.xml similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_letters_german.xml rename to lib-keyboard/src/main/res/xml/keys_letters_german.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_letters_greek.xml b/lib-keyboard/src/main/res/xml/keys_letters_greek.xml similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_letters_greek.xml rename to lib-keyboard/src/main/res/xml/keys_letters_greek.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_letters_lithuanian.xml b/lib-keyboard/src/main/res/xml/keys_letters_lithuanian.xml similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_letters_lithuanian.xml rename to lib-keyboard/src/main/res/xml/keys_letters_lithuanian.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_letters_programmer.xml b/lib-keyboard/src/main/res/xml/keys_letters_programmer.xml similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_letters_programmer.xml rename to lib-keyboard/src/main/res/xml/keys_letters_programmer.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_letters_qwerty.xml b/lib-keyboard/src/main/res/xml/keys_letters_qwerty.xml similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_letters_qwerty.xml rename to lib-keyboard/src/main/res/xml/keys_letters_qwerty.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_letters_romanian.xml b/lib-keyboard/src/main/res/xml/keys_letters_romanian.xml similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_letters_romanian.xml rename to lib-keyboard/src/main/res/xml/keys_letters_romanian.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_letters_russian.xml b/lib-keyboard/src/main/res/xml/keys_letters_russian.xml similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_letters_russian.xml rename to lib-keyboard/src/main/res/xml/keys_letters_russian.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_letters_slovenian.xml b/lib-keyboard/src/main/res/xml/keys_letters_slovenian.xml similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_letters_slovenian.xml rename to lib-keyboard/src/main/res/xml/keys_letters_slovenian.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_letters_spanish_qwerty.xml b/lib-keyboard/src/main/res/xml/keys_letters_spanish_qwerty.xml similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_letters_spanish_qwerty.xml rename to lib-keyboard/src/main/res/xml/keys_letters_spanish_qwerty.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_letters_turkish_q.xml b/lib-keyboard/src/main/res/xml/keys_letters_turkish_q.xml similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_letters_turkish_q.xml rename to lib-keyboard/src/main/res/xml/keys_letters_turkish_q.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_number.xml b/lib-keyboard/src/main/res/xml/keys_number.xml old mode 100755 new mode 100644 similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_number.xml rename to lib-keyboard/src/main/res/xml/keys_number.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_popup_template.xml b/lib-keyboard/src/main/res/xml/keys_popup_template.xml similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_popup_template.xml rename to lib-keyboard/src/main/res/xml/keys_popup_template.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_symbols.xml b/lib-keyboard/src/main/res/xml/keys_symbols.xml old mode 100755 new mode 100644 similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_symbols.xml rename to lib-keyboard/src/main/res/xml/keys_symbols.xml diff --git a/frogo-keyboard/src/main/res/xml/keys_symbols_shift.xml b/lib-keyboard/src/main/res/xml/keys_symbols_shift.xml old mode 100755 new mode 100644 similarity index 100% rename from frogo-keyboard/src/main/res/xml/keys_symbols_shift.xml rename to lib-keyboard/src/main/res/xml/keys_symbols_shift.xml diff --git a/frogo-keyboard/src/test/java/com/frogobox/libkeyboard/BaseKeyboardIMETest.kt b/lib-keyboard/src/test/java/com/frogobox/libkeyboard/BaseKeyboardIMETest.kt similarity index 100% rename from frogo-keyboard/src/test/java/com/frogobox/libkeyboard/BaseKeyboardIMETest.kt rename to lib-keyboard/src/test/java/com/frogobox/libkeyboard/BaseKeyboardIMETest.kt diff --git a/lib-keyboard/src/test/java/com/frogobox/libkeyboard/EmojiKeyboardComposeTest.kt b/lib-keyboard/src/test/java/com/frogobox/libkeyboard/EmojiKeyboardComposeTest.kt new file mode 100644 index 0000000..10562ec --- /dev/null +++ b/lib-keyboard/src/test/java/com/frogobox/libkeyboard/EmojiKeyboardComposeTest.kt @@ -0,0 +1,78 @@ +package com.frogobox.libkeyboard + +import com.frogobox.libkeyboard.ui.emoji.EmojiCategoryType +import com.frogobox.libkeyboard.ui.emoji.displayName +import com.frogobox.libkeyboard.ui.emoji.getEmojiCategory +import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class EmojiKeyboardComposeTest { + + @Test + fun testEmojiCategoryTypeCountAndPaths() { + val categories = EmojiCategoryType.entries + assertEquals(10, categories.size) + + assertTrue(categories.any { it == EmojiCategoryType.GENERAL && it.path == "media/_emoji_general.txt" }) + assertTrue(categories.any { it == EmojiCategoryType.ACTIVITIES && it.path == "media/emoji_activities.txt" }) + assertTrue(categories.any { it == EmojiCategoryType.ANIMAL_NATURE && it.path == "media/emoji_animal_nature.txt" }) + assertTrue(categories.any { it == EmojiCategoryType.FLAG && it.path == "media/emoji_flag.txt" }) + assertTrue(categories.any { it == EmojiCategoryType.FOOD_DRINK && it.path == "media/emoji_food_drink.txt" }) + assertTrue(categories.any { it == EmojiCategoryType.OBJECTS && it.path == "media/emoji_objects.txt" }) + assertTrue(categories.any { it == EmojiCategoryType.PEOPLE_BODY && it.path == "media/emoji_people_body.txt" }) + assertTrue(categories.any { it == EmojiCategoryType.SMILEYS_EMOTION && it.path == "media/emoji_smileys_emotion.txt" }) + assertTrue(categories.any { it == EmojiCategoryType.SYMBOLS && it.path == "media/emoji_symbols.txt" }) + assertTrue(categories.any { it == EmojiCategoryType.TRAVEL_PLACES && it.path == "media/emoji_travel_places.txt" }) + } + + @Test + fun testEmojiCategoryTypeDisplayNames() { + assertEquals("General", EmojiCategoryType.GENERAL.displayName) + assertEquals("Activities", EmojiCategoryType.ACTIVITIES.displayName) + assertEquals("Animals & Nature", EmojiCategoryType.ANIMAL_NATURE.displayName) + assertEquals("Flags", EmojiCategoryType.FLAG.displayName) + assertEquals("Food & Drink", EmojiCategoryType.FOOD_DRINK.displayName) + assertEquals("Objects", EmojiCategoryType.OBJECTS.displayName) + assertEquals("People & Body", EmojiCategoryType.PEOPLE_BODY.displayName) + assertEquals("Smileys & Emotion", EmojiCategoryType.SMILEYS_EMOTION.displayName) + assertEquals("Symbols", EmojiCategoryType.SYMBOLS.displayName) + assertEquals("Travel & Places", EmojiCategoryType.TRAVEL_PLACES.displayName) + } + + @Test + fun testGetEmojiCategoryListMapping() { + val categoryList = getEmojiCategory() + assertEquals(10, categoryList.size) + + val first = categoryList[0] + assertEquals("GENERAL", first.name) + assertNotNull(first.icon) + assertEquals("media/_emoji_general.txt", first.path) + } + + @Test + fun testKeycodeMappingsForComposeKeyboard() { + assertEquals(-1, ItemMainKeyboard.KEYCODE_SHIFT) + assertEquals(-2, ItemMainKeyboard.KEYCODE_MODE_CHANGE) + assertEquals(-4, ItemMainKeyboard.KEYCODE_ENTER) + assertEquals(-5, ItemMainKeyboard.KEYCODE_DELETE) + assertEquals(32, ItemMainKeyboard.KEYCODE_SPACE) + assertEquals(-6, ItemMainKeyboard.KEYCODE_EMOJI) + assertEquals(-7, ItemMainKeyboard.KEYCODE_TAB) + } + + @Test + fun testScrollTriggerIncrementLogic() { + var resetTrigger = 0 + assertEquals(0, resetTrigger) + + resetTrigger++ + assertEquals(1, resetTrigger) + + resetTrigger++ + assertEquals(2, resetTrigger) + } +} diff --git a/frogo-keyboard/src/test/java/com/frogobox/libkeyboard/KeyboardLayoutLogicTest.kt b/lib-keyboard/src/test/java/com/frogobox/libkeyboard/KeyboardLayoutLogicTest.kt similarity index 100% rename from frogo-keyboard/src/test/java/com/frogobox/libkeyboard/KeyboardLayoutLogicTest.kt rename to lib-keyboard/src/test/java/com/frogobox/libkeyboard/KeyboardLayoutLogicTest.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index d3e65ed..5d88077 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,4 +16,4 @@ dependencyResolutionManagement { } } include(":app") -include(":frogo-keyboard") +include(":lib-keyboard") From 760b29b414795ce46e935e5a779bdb6a74453ce0 Mon Sep 17 00:00:00 2001 From: amirisback Date: Sun, 20 Sep 2026 10:52:53 +0700 Subject: [PATCH 5/8] feat: Jetpack compose --- .../appkeyboard/services/KeyboardIME.kt | 776 +++++++++--------- .../ui/keyboard/root/KeyboardFeatureHeader.kt | 112 +++ .../ui/keyboard/root/KeyboardImeRootScreen.kt | 253 ++++++ .../ui/keyboard/root/KeyboardPanelState.kt | 62 ++ .../ui/keyboard/root/KeyboardSuggestionBar.kt | 230 ++++++ .../appkeyboard/KeyboardImeComposeUnitTest.kt | 160 ++++ lib-keyboard/build.gradle.kts | 8 + .../common/core/BaseKeyboardIME.kt | 29 +- .../libkeyboard/ui/main/MainKeyboard.kt | 39 +- .../libkeyboard/ui/main/MainKeyboardScreen.kt | 5 + .../libkeyboard/MainKeyboardBackgroundTest.kt | 119 +++ 11 files changed, 1407 insertions(+), 386 deletions(-) create mode 100644 app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardFeatureHeader.kt create mode 100644 app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardImeRootScreen.kt create mode 100644 app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardPanelState.kt create mode 100644 app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardSuggestionBar.kt create mode 100644 app/src/test/java/com/frogobox/appkeyboard/KeyboardImeComposeUnitTest.kt create mode 100644 lib-keyboard/src/test/java/com/frogobox/libkeyboard/MainKeyboardBackgroundTest.kt diff --git a/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt b/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt index e10d588..38a484e 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt @@ -1,40 +1,71 @@ package com.frogobox.appkeyboard.services import android.content.Intent -import android.view.LayoutInflater -import android.view.ViewGroup +import android.graphics.Paint +import android.graphics.Typeface +import android.view.View import android.view.inputmethod.InputConnection import android.view.inputmethod.InputMethodManager import android.widget.EditText +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.emoji2.text.EmojiCompat +import androidx.lifecycle.setViewTreeLifecycleOwner +import androidx.lifecycle.setViewTreeViewModelStoreOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import com.frogobox.api.movie.ConsumeMovieApi +import com.frogobox.api.news.ConsumeNewsApi import com.frogobox.appkeyboard.R -import com.frogobox.appkeyboard.databinding.ItemKeyboardHeaderBinding -import com.frogobox.appkeyboard.databinding.KeyboardImeBinding +import com.frogobox.appkeyboard.model.AutoTextEntity import com.frogobox.appkeyboard.model.KeyboardFeatureModel import com.frogobox.appkeyboard.model.KeyboardFeatureType import com.frogobox.appkeyboard.model.ThemeType +import com.frogobox.appkeyboard.suggestion.SuggestionResult +import com.frogobox.appkeyboard.suggestion.WordSuggestionEngine +import com.frogobox.appkeyboard.ui.autotext.AutoTextActivity +import com.frogobox.appkeyboard.ui.keyboard.autotext.AutoTextKeyboardViewModel +import com.frogobox.appkeyboard.ui.keyboard.root.KeyboardImeRootScreen +import com.frogobox.appkeyboard.ui.keyboard.root.KeyboardPanelState import com.frogobox.appkeyboard.ui.main.MainActivity +import com.frogobox.appkeyboard.ui.theme.compose.FrogoKeyboardTheme +import com.frogobox.coreapi.ConsumeApiResponse +import com.frogobox.coresdk.response.FrogoDataResponse +import com.frogobox.coreutil.movie.MovieUrl +import com.frogobox.coreutil.movie.model.TrendingMovie +import com.frogobox.coreutil.movie.response.Trending +import com.frogobox.coreutil.news.NewsConstant.CATEGORY_HEALTH +import com.frogobox.coreutil.news.NewsConstant.COUNTRY_ID +import com.frogobox.coreutil.news.NewsUrl +import com.frogobox.coreutil.news.model.Article +import com.frogobox.coreutil.news.response.ArticleResponse import com.frogobox.libkeyboard.common.core.BaseKeyboardIME import com.frogobox.libkeyboard.common.sound.MechanicalSoundManager import com.frogobox.libkeyboard.common.sound.MechanicalSoundType +import com.frogobox.libkeyboard.ui.emoji.EmojiCategoryType +import com.frogobox.libkeyboard.ui.emoji.getEmojiCategory +import com.frogobox.libkeyboard.ui.emoji.parseRawEmojiSpecsFile import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard -import com.frogobox.recycler.core.FrogoRecyclerNotifyListener -import com.frogobox.recycler.core.IFrogoBindingAdapter -import com.frogobox.recycler.ext.injectorBinding +import com.frogobox.libkeyboard.ui.main.MainKeyboard import com.frogobox.sdk.delegate.preference.PreferenceDelegates -import com.frogobox.sdk.ext.getColorExt -import com.frogobox.sdk.ext.gone -import com.frogobox.sdk.ext.invisible -import com.frogobox.appkeyboard.suggestion.WordSuggestionEngine -import com.frogobox.sdk.ext.visible -import androidx.lifecycle.setViewTreeLifecycleOwner -import androidx.lifecycle.setViewTreeViewModelStoreOwner -import androidx.savedstate.setViewTreeSavedStateRegistryOwner import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject - +/** + * Modern 100% Full Pure Jetpack Compose InputMethodService. + * Replaces legacy ViewBinding (keyboard_ime.xml, item_keyboard_header.xml, layout_keyboard_suggestion.xml) + * with a unified ComposeView, reactive panel navigation, and direct Compose feature integrations. + */ @AndroidEntryPoint -class KeyboardIME : BaseKeyboardIME() { +class KeyboardIME : BaseKeyboardIME() { private val imeLifecycleOwner = ImeLifecycleOwner() @@ -47,164 +78,37 @@ class KeyboardIME : BaseKeyboardIME() { @Inject lateinit var suggestionEngine: WordSuggestionEngine - override fun setupViewBinding(): KeyboardImeBinding { - return KeyboardImeBinding.inflate(LayoutInflater.from(this), null, false) - } - - private val featureKeyboardCallback = object : - IFrogoBindingAdapter { - - override fun areContentsTheSame( - oldItem: KeyboardFeatureModel, - newItem: KeyboardFeatureModel - ): Boolean { - return oldItem == newItem - } - - override fun areItemsTheSame( - oldItem: KeyboardFeatureModel, - newItem: KeyboardFeatureModel - ): Boolean { - return oldItem.id == newItem.id - } - - override fun setViewBinding(parent: ViewGroup): ItemKeyboardHeaderBinding { - return ItemKeyboardHeaderBinding.inflate( - LayoutInflater.from(parent.context), parent, false - ) - } - - override fun setupInitComponent( - binding: ItemKeyboardHeaderBinding, - data: KeyboardFeatureModel, - position: Int, - notifyListener: FrogoRecyclerNotifyListener, - ) { - binding.ivIcon.setImageResource(data.icon) - binding.tvTitle.text = data.text - - if (getStateToggle(data.id)) { - binding.root.visible() - } else { - binding.root.gone() - } - - } - - override fun onItemClicked( - binding: ItemKeyboardHeaderBinding, - data: KeyboardFeatureModel, - position: Int, - notifyListener: FrogoRecyclerNotifyListener, - ) { - - when (KeyboardFeatureType.from(data.id)) { - KeyboardFeatureType.NEWS -> { - hideMainKeyboard() - this@KeyboardIME.binding?.keyboardNews?.visible() - } - - KeyboardFeatureType.MOVIE -> { - hideMainKeyboard() - this@KeyboardIME.binding?.keyboardMoview?.visible() - } - - KeyboardFeatureType.WEB -> { - this@KeyboardIME.binding?.keyboardHeader?.gone() - this@KeyboardIME.binding?.keyboardWebview?.visible() - } - - KeyboardFeatureType.FORM -> { - this@KeyboardIME.binding?.keyboardHeader?.gone() - this@KeyboardIME.binding?.keyboardForm?.visible() - } - - KeyboardFeatureType.AUTO_TEXT -> { - hideMainKeyboard() - this@KeyboardIME.binding?.keyboardAutotext?.visible() - } - - KeyboardFeatureType.TEMPLATE_TEXT_GAME -> { - hideMainKeyboard() - this@KeyboardIME.binding?.keyboardTemplateText?.setupTemplateTextType(KeyboardFeatureType.TEMPLATE_TEXT_GAME) - this@KeyboardIME.binding?.keyboardTemplateText?.visible() - } - - KeyboardFeatureType.TEMPLATE_TEXT_APP -> { - hideMainKeyboard() - this@KeyboardIME.binding?.keyboardTemplateText?.setupTemplateTextType(KeyboardFeatureType.TEMPLATE_TEXT_APP) - this@KeyboardIME.binding?.keyboardTemplateText?.visible() - } - - KeyboardFeatureType.TEMPLATE_TEXT_SALE -> { - hideMainKeyboard() - this@KeyboardIME.binding?.keyboardTemplateText?.setupTemplateTextType(KeyboardFeatureType.TEMPLATE_TEXT_SALE) - this@KeyboardIME.binding?.keyboardTemplateText?.visible() - } - - KeyboardFeatureType.TEMPLATE_TEXT_LOVE -> { - hideMainKeyboard() - this@KeyboardIME.binding?.keyboardTemplateText?.setupTemplateTextType(KeyboardFeatureType.TEMPLATE_TEXT_LOVE) - this@KeyboardIME.binding?.keyboardTemplateText?.visible() - } - - KeyboardFeatureType.TEMPLATE_TEXT_GREETING -> { - hideMainKeyboard() - this@KeyboardIME.binding?.keyboardTemplateText?.setupTemplateTextType(KeyboardFeatureType.TEMPLATE_TEXT_GREETING) - this@KeyboardIME.binding?.keyboardTemplateText?.visible() - } - - KeyboardFeatureType.SUGGESTION -> { - this@KeyboardIME.showSuggestionBar() - } - - KeyboardFeatureType.CHANGE_KEYBOARD -> { - (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager).showInputMethodPicker() - } - - KeyboardFeatureType.SETTING -> { - binding.root.context.startActivity(Intent( - binding.root.context, MainActivity::class.java - ).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - }) - } - - } - - } - - } - - override fun setupTheme() { - binding?.apply { - - val background = pref.getPrefInt( - KeyboardUtil.KEYBOARD_COLOR, - R.color.color_bg_keyboard_default - ) - - val typeString = pref.getPrefString( - KeyboardUtil.KEYBOARD_COLOR_TYPE, - ThemeType.COLOR.name - ) - - val backgroundType = runCatching { - ThemeType.valueOf(typeString) - }.getOrDefault(ThemeType.COLOR) - - when (backgroundType) { - ThemeType.COLOR -> { - ivBackgroundKeyboard.setImageDrawable(null) - ivBackgroundKeyboard.setBackgroundColor(getColorExt(background)) - } - ThemeType.IMAGE -> { - ivBackgroundKeyboard.setBackgroundColor(android.graphics.Color.TRANSPARENT) - ivBackgroundKeyboard.setImageResource(background) - } - } - } - } + // Reactive State Holders + private val activePanelStateFlow = MutableStateFlow(KeyboardPanelState.MAIN) + private val themeTypeFlow = MutableStateFlow(ThemeType.COLOR) + private val themeBackgroundResFlow = MutableStateFlow(R.color.color_bg_keyboard_default) + private val featuresFlow = MutableStateFlow>(emptyList()) + private val isSuggestionVisibleFlow = MutableStateFlow(false) + private val suggestionResultFlow = MutableStateFlow(SuggestionResult.EMPTY) + + // Sub-Screen Reactive Data Flows + private val autoTextListFlow = MutableStateFlow>(emptyList()) + private val newsArticlesFlow = MutableStateFlow>(emptyList()) + private val isNewsLoadingFlow = MutableStateFlow(false) + private val movieListFlow = MutableStateFlow>(emptyList()) + private val isMovieLoadingFlow = MutableStateFlow(false) + + // Emoji State Flows + private val emojisFlow = MutableStateFlow>(emptyList()) + private val selectedEmojiCategoryFlow = MutableStateFlow(EmojiCategoryType.GENERAL) + private val isEmojiLoadingFlow = MutableStateFlow(false) + private val emojiCache = mutableMapOf>() + private val systemFontPaint = Paint().apply { typeface = Typeface.DEFAULT } + private val emojiCompatMetadataVersion = 0 + + // Coroutine Scope & View Models + private val serviceJob = SupervisorJob() + private val serviceScope = CoroutineScope(Dispatchers.Main + serviceJob) + private val autoTextViewModel by lazy { AutoTextKeyboardViewModel(this) } + + // Interop MainKeyboard Instance + private var mainKeyboardInstance: MainKeyboard? = null + private val currentKeyboardFlow = MutableStateFlow(null) override fun onCreate() { super.onCreate() @@ -216,6 +120,10 @@ class KeyboardIME : BaseKeyboardIME() { imeLifecycleOwner.onStart() imeLifecycleOwner.onResume() applySoundAndHapticSettings() + setupTheme() + setupFeatureKeyboard() + loadAutoText() + showMainKeyboard() } override fun onWindowHidden() { @@ -224,207 +132,181 @@ class KeyboardIME : BaseKeyboardIME() { imeLifecycleOwner.onStop() } - override fun initialSetupKeyboard() { - applySoundAndHapticSettings() - binding?.keyboardMain?.setKeyboard(keyboard!!) + override fun onDestroy() { + super.onDestroy() + serviceJob.cancelChildren() + imeLifecycleOwner.onDestroy() + mainKeyboardInstance = null + emojiCache.clear() + MechanicalSoundManager.getInstance(this).release() } - override fun setupBinding() { - super.setupBinding() - binding?.apply { - keyboardMain.mOnKeyboardActionListener = this@KeyboardIME - keyboardEmoji.mOnKeyboardActionListener = this@KeyboardIME - - root.setViewTreeLifecycleOwner(imeLifecycleOwner) - root.setViewTreeViewModelStoreOwner(imeLifecycleOwner) - root.setViewTreeSavedStateRegistryOwner(imeLifecycleOwner) + override fun onCreateInputView(): View { + setupBinding() + initCurrentInputConnection() + initView() + + val composeView = ComposeView(this).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool) + setViewTreeLifecycleOwner(imeLifecycleOwner) + setViewTreeViewModelStoreOwner(imeLifecycleOwner) + setViewTreeSavedStateRegistryOwner(imeLifecycleOwner) + setContent { + FrogoKeyboardTheme { + val activePanel by activePanelStateFlow.collectAsState() + val themeType by themeTypeFlow.collectAsState() + val themeBg by themeBackgroundResFlow.collectAsState() + val features by featuresFlow.collectAsState() + val isSuggestionVisible by isSuggestionVisibleFlow.collectAsState() + val suggestionResult by suggestionResultFlow.collectAsState() + val currentKeyboard by currentKeyboardFlow.collectAsState() + + val autoTextList by autoTextListFlow.collectAsState() + val newsArticles by newsArticlesFlow.collectAsState() + val isNewsLoading by isNewsLoadingFlow.collectAsState() + val movieList by movieListFlow.collectAsState() + val isMovieLoading by isMovieLoadingFlow.collectAsState() + + val emojis by emojisFlow.collectAsState() + val selectedEmojiCategory by selectedEmojiCategoryFlow.collectAsState() + val isEmojiLoading by isEmojiLoadingFlow.collectAsState() + + KeyboardImeRootScreen( + activePanelState = activePanel, + themeType = themeType, + themeBackgroundRes = themeBg, + features = features, + onFeatureClick = { feature -> handleFeatureClick(feature) }, + isSuggestionVisible = isSuggestionVisible, + suggestionResult = suggestionResult, + onCandidateSelected = { word, _ -> handleCandidateSelected(word) }, + onSwitchSuggestionMenu = { + isSuggestionVisibleFlow.value = false + }, + onCloseSuggestion = { + suggestionResultFlow.value = SuggestionResult.EMPTY + isSuggestionVisibleFlow.value = false + }, + currentKeyboard = currentKeyboard, + onKeyboardActionListener = this@KeyboardIME, + onMainKeyboardInit = { view -> + mainKeyboardInstance = view + currentKeyboard?.let { view.setKeyboard(it) } + }, + autoTextList = autoTextList, + onManageAutoText = { + val intent = Intent(this@KeyboardIME, AutoTextActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + startActivity(intent) + }, + newsArticles = newsArticles, + isNewsLoading = isNewsLoading, + movieList = movieList, + isMovieLoading = isMovieLoading, + emojis = emojis, + selectedEmojiCategory = selectedEmojiCategory, + emojiCategories = getEmojiCategory(), + isEmojiLoading = isEmojiLoading, + onSelectEmojiCategory = { cat -> + selectedEmojiCategoryFlow.value = cat + loadEmojis(cat.path) + }, + onEmojiClicked = { emoji -> + getActiveInputConnection()?.commitText(emoji, 1) + this@KeyboardIME.onText(emoji) + this@KeyboardIME.onActionUp() + }, + onCommitText = { text -> + getActiveInputConnection()?.commitText(text, 1) + }, + onBackToMain = { + showMainKeyboard() + } + ) + } + } } + window?.window?.decorView?.let { decor -> decor.setViewTreeLifecycleOwner(imeLifecycleOwner) decor.setViewTreeViewModelStoreOwner(imeLifecycleOwner) decor.setViewTreeSavedStateRegistryOwner(imeLifecycleOwner) } - } - override fun invalidateKeyboard() { - binding?.keyboardAutotext?.initData() - setupFeatureKeyboard() + return composeView } - override fun initCurrentInputConnection() { - binding?.apply { - keyboardAutotext.setInputConnection(currentInputConnection) - keyboardNews.setInputConnection(currentInputConnection) - keyboardMoview.setInputConnection(currentInputConnection) - keyboardWebview.setInputConnection(currentInputConnection) - keyboardForm.setInputConnection(currentInputConnection) - keyboardEmoji.setInputConnection(currentInputConnection) - keyboardTemplateText.setInputConnection(currentInputConnection) + override fun initialSetupKeyboard() { + applySoundAndHapticSettings() + currentKeyboardFlow.value = keyboard + mainKeyboardInstance?.let { view -> + keyboard?.let { view.setKeyboard(it) } } } - override fun hideMainKeyboard() { - binding?.apply { - keyboardMain.invisible() - keyboardHeader.invisible() - keyboardSuggestion.gone() - } + override fun invalidateAllKeys() { + mainKeyboardInstance?.invalidateAllKeys() } override fun showMainKeyboard() { - binding?.apply { - keyboardMain.visible() - keyboardSuggestion.gone() - if (keyboardUtil.menuKeyboard().isEmpty()) { - keyboardHeader.gone() - } else { - keyboardHeader.visible() - } - keyboardAutotext.gone() - keyboardNews.gone() - keyboardMoview.gone() - keyboardWebview.gone() - keyboardForm.gone() - keyboardEmoji.gone() - keyboardEmoji.resetScroll() - } + activePanelStateFlow.value = KeyboardPanelState.MAIN + isSuggestionVisibleFlow.value = false + suggestionResultFlow.value = SuggestionResult.EMPTY + } + + override fun hideMainKeyboard() { + // Managed declaratively via activePanelStateFlow transitions } override fun showOnlyKeyboard() { - binding?.keyboardMain?.visible() + activePanelStateFlow.value = KeyboardPanelState.MAIN } override fun hideOnlyKeyboard() { - binding?.keyboardMain?.gone() + // Declarative state management } - override fun EditText.showKeyboardExt() { - setOnFocusChangeListener { _, hasFocus -> - if (hasFocus) { - showOnlyKeyboard() - } - } - setOnClickListener { - showOnlyKeyboard() - } + override fun runEmojiBoard() { + activePanelStateFlow.value = KeyboardPanelState.EMOJI + openEmojiPalette() } - override fun initBackToMainKeyboard() { - binding?.apply { - keyboardAutotext.setOnBackClickListener { - keyboardAutotext.gone() - showMainKeyboard() - } - - keyboardNews.setOnBackClickListener { - keyboardNews.gone() - showMainKeyboard() - } - - keyboardMoview.setOnBackClickListener { - keyboardMoview.gone() - showMainKeyboard() - } - - keyboardWebview.setOnBackClickListener { - keyboardWebview.gone() - showMainKeyboard() - } - - keyboardForm.setOnBackClickListener { - keyboardForm.gone() - showMainKeyboard() - } + override fun setupTheme() { + val background = pref.getPrefInt( + KeyboardUtil.KEYBOARD_COLOR, + R.color.color_bg_keyboard_default + ) - keyboardEmoji.setOnBackClickListener { - keyboardEmoji.gone() - keyboardEmoji.resetScroll() - showMainKeyboard() - } + val typeString = pref.getPrefString( + KeyboardUtil.KEYBOARD_COLOR_TYPE, + ThemeType.COLOR.name + ) - keyboardTemplateText.setOnBackClickListener { - keyboardTemplateText.gone() - showMainKeyboard() - } + val backgroundType = runCatching { + ThemeType.valueOf(typeString) + }.getOrDefault(ThemeType.COLOR) - } + themeTypeFlow.value = backgroundType + themeBackgroundResFlow.value = background } override fun setupFeatureKeyboard() { - val maxMenu = 4 - val gridSize = if (keyboardUtil.menuKeyboard().size <= maxMenu) { - keyboardUtil.menuKeyboard().size - } else if (keyboardUtil.menuKeyboard().size.mod(maxMenu) == 0) { - maxMenu - } else { - maxMenu + 1 - } - - binding?.apply { - if (keyboardUtil.menuKeyboard().isEmpty()) { - keyboardHeader.gone() - } else { - keyboardHeader.visible() - keyboardHeader.injectorBinding() - .addData(keyboardUtil.menuKeyboard()) - .addCallback(featureKeyboardCallback) - .createLayoutGrid(gridSize).build() - } - } + featuresFlow.value = keyboardUtil.menuKeyboard() } - - private fun getActiveInputConnection(): InputConnection? { - return currentInputConnection - } - - private fun getWordBeforeCursor(ic: InputConnection): String { - val text = ic.getTextBeforeCursor(40, 0)?.toString() ?: "" - return text.takeLastWhile { it.isLetterOrDigit() || it == '\'' } - } - - private fun setupSuggestionBar() { - binding?.keyboardSuggestion?.apply { - onCandidateSelected = { selectedWord, _ -> - val ic = getActiveInputConnection() - if (ic != null) { - val currentWord = getWordBeforeCursor(ic) - if (currentWord.isNotEmpty()) { - ic.deleteSurroundingText(currentWord.length, 0) - } - ic.commitText("$selectedWord ", 1) - clearSuggestions() - showFeatureHeader() - } - } - - onSwitchMenuClicked = { - showFeatureHeader() - } - - onCloseClicked = { - clearSuggestions() - showFeatureHeader() - } - } - } - - private fun showSuggestionBar() { - binding?.apply { - keyboardHeader.gone() - keyboardSuggestion.visible() - } + override fun invalidateKeyboard() { + setupTheme() + setupFeatureKeyboard() + loadAutoText() } - private fun showFeatureHeader() { - binding?.apply { - keyboardSuggestion.gone() - if (keyboardUtil.menuKeyboard().isEmpty()) { - keyboardHeader.gone() - } else { - keyboardHeader.visible() - } - } + override fun initView() { + suggestionEngine.loadDictionaryFromAsset(this) + setupTheme() + setupFeatureKeyboard() + loadAutoText() + openEmojiPalette() } override fun onKey(code: Int) { @@ -435,14 +317,14 @@ class KeyboardIME : BaseKeyboardIME() { val word = getWordBeforeCursor(ic) if (word.isNotEmpty()) { val suggestions = suggestionEngine.getSuggestions(word) - binding?.keyboardSuggestion?.setSuggestions(suggestions) - showSuggestionBar() + suggestionResultFlow.value = suggestions + isSuggestionVisibleFlow.value = true } else { - binding?.keyboardSuggestion?.clearSuggestions() + suggestionResultFlow.value = SuggestionResult.EMPTY if (code == ItemMainKeyboard.KEYCODE_SPACE || code == ItemMainKeyboard.KEYCODE_ENTER || code == ItemMainKeyboard.KEYCODE_DELETE) { - showFeatureHeader() + isSuggestionVisibleFlow.value = false } } } @@ -480,33 +362,199 @@ class KeyboardIME : BaseKeyboardIME() { } } - override fun initView() { - suggestionEngine.loadDictionaryFromAsset(this) - setupSuggestionBar() - setupFeatureKeyboard() - initBackToMainKeyboard() + override fun getKeyboardLayoutXML(): Int { + return pref.getPrefInt( + KeyboardUtil.KEYBOARD_TYPE, com.frogobox.libkeyboard.R.xml.keys_letters_qwerty + ) } - override fun invalidateAllKeys() { - binding?.keyboardMain?.invalidateAllKeys() + override fun EditText.showKeyboardExt() { + setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + showOnlyKeyboard() + } + } + setOnClickListener { + showOnlyKeyboard() + } } - - override fun runEmojiBoard() { - binding?.keyboardEmoji?.visible() - binding?.keyboardMain?.invisible() - binding?.keyboardHeader?.gone() - binding?.keyboardEmoji?.openEmojiPalette() + private fun getActiveInputConnection(): InputConnection? { + return currentInputConnection } - override fun getKeyboardLayoutXML(): Int { - return pref.getPrefInt( - KeyboardUtil.KEYBOARD_TYPE, com.frogobox.libkeyboard.R.xml.keys_letters_qwerty + private fun getWordBeforeCursor(ic: InputConnection): String { + val text = ic.getTextBeforeCursor(40, 0)?.toString() ?: "" + return text.takeLastWhile { it.isLetterOrDigit() || it == '\'' } + } + + private fun handleCandidateSelected(selectedWord: String) { + val ic = getActiveInputConnection() + if (ic != null) { + val currentWord = getWordBeforeCursor(ic) + if (currentWord.isNotEmpty()) { + ic.deleteSurroundingText(currentWord.length, 0) + } + ic.commitText("$selectedWord ", 1) + suggestionResultFlow.value = SuggestionResult.EMPTY + isSuggestionVisibleFlow.value = false + } + } + + private fun handleFeatureClick(feature: KeyboardFeatureModel) { + val featureType = KeyboardFeatureType.from(feature.id) + when (featureType) { + KeyboardFeatureType.AUTO_TEXT -> { + loadAutoText() + activePanelStateFlow.value = KeyboardPanelState.AUTO_TEXT + } + + KeyboardFeatureType.TEMPLATE_TEXT_GAME, + KeyboardFeatureType.TEMPLATE_TEXT_APP, + KeyboardFeatureType.TEMPLATE_TEXT_SALE, + KeyboardFeatureType.TEMPLATE_TEXT_LOVE, + KeyboardFeatureType.TEMPLATE_TEXT_GREETING -> { + KeyboardPanelState.fromFeature(featureType)?.let { panel -> + activePanelStateFlow.value = panel + } + } + + KeyboardFeatureType.NEWS -> { + fetchNews() + activePanelStateFlow.value = KeyboardPanelState.NEWS + } + + KeyboardFeatureType.MOVIE -> { + fetchMovies() + activePanelStateFlow.value = KeyboardPanelState.MOVIE + } + + KeyboardFeatureType.WEB -> { + activePanelStateFlow.value = KeyboardPanelState.WEBVIEW + } + + KeyboardFeatureType.FORM -> { + activePanelStateFlow.value = KeyboardPanelState.FORM + } + + KeyboardFeatureType.SUGGESTION -> { + isSuggestionVisibleFlow.value = !isSuggestionVisibleFlow.value + } + + KeyboardFeatureType.CHANGE_KEYBOARD -> { + (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager).showInputMethodPicker() + } + + KeyboardFeatureType.SETTING -> { + startActivity(Intent(this, MainActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }) + } + } + } + + private fun loadAutoText() { + autoTextViewModel.getAutoText { items -> + autoTextListFlow.value = items + } + } + + private fun fetchNews() { + isNewsLoadingFlow.value = true + val consumeNewsApi = ConsumeNewsApi(NewsUrl.API_KEY) + consumeNewsApi.getTopHeadline( + null, + null, + CATEGORY_HEALTH, + COUNTRY_ID, + null, + null, + object : ConsumeApiResponse { + override fun onSuccess(data: ArticleResponse) { + isNewsLoadingFlow.value = false + data.articles?.let { newsArticlesFlow.value = it } + } + + override fun onFailed(statusCode: Int, errorMessage: String) { + isNewsLoadingFlow.value = false + } + + override fun onFinish() { + isNewsLoadingFlow.value = false + } + + override fun onShowProgress() { + isNewsLoadingFlow.value = true + } + + override fun onHideProgress() { + isNewsLoadingFlow.value = false + } + } ) } - private fun getStateToggle(key: String): Boolean { - return pref.getPrefBoolean(key, true) + private fun fetchMovies() { + isMovieLoadingFlow.value = true + val consumeMovieApi = ConsumeMovieApi(MovieUrl.API_KEY) + consumeMovieApi.getTrendingMovieDay(object : FrogoDataResponse> { + override fun onSuccess(data: Trending) { + isMovieLoadingFlow.value = false + data.results?.let { movieListFlow.value = it } + } + + override fun onFailed(statusCode: Int, errorMessage: String) { + isMovieLoadingFlow.value = false + } + + override fun onFinish() { + isMovieLoadingFlow.value = false + } + + override fun onShowProgress() { + isMovieLoadingFlow.value = true + } + + override fun onHideProgress() { + isMovieLoadingFlow.value = false + } + }) + } + + private fun openEmojiPalette() { + selectedEmojiCategoryFlow.value = EmojiCategoryType.GENERAL + loadEmojis(EmojiCategoryType.GENERAL.path) + } + + private fun loadEmojis(path: String) { + val cached = emojiCache[path] + if (cached != null) { + emojisFlow.value = cached + return + } + + isEmojiLoadingFlow.value = true + serviceScope.launch(Dispatchers.IO) { + val fullEmojiList = parseRawEmojiSpecsFile(this@KeyboardIME, path) + + val isEmojiCompatReady = EmojiCompat.isConfigured() && + EmojiCompat.get().loadState == EmojiCompat.LOAD_STATE_SUCCEEDED + + val emojis = fullEmojiList.filter { emoji -> + systemFontPaint.hasGlyph(emoji) || (isEmojiCompatReady && + EmojiCompat.get().getEmojiMatch( + emoji, + emojiCompatMetadataVersion + ) == EmojiCompat.EMOJI_SUPPORTED) + } + + emojiCache[path] = emojis + + withContext(Dispatchers.Main) { + emojisFlow.value = emojis + isEmojiLoadingFlow.value = false + } + } } private fun applySoundAndHapticSettings() { @@ -524,10 +572,4 @@ class KeyboardIME : BaseKeyboardIME() { ItemMainKeyboard.VIBRATE_ON_KEYPRESS = vibrateEnabled } - override fun onDestroy() { - super.onDestroy() - imeLifecycleOwner.onDestroy() - MechanicalSoundManager.getInstance(this).release() - } - } \ No newline at end of file diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardFeatureHeader.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardFeatureHeader.kt new file mode 100644 index 0000000..3e2dda9 --- /dev/null +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardFeatureHeader.kt @@ -0,0 +1,112 @@ +package com.frogobox.appkeyboard.ui.keyboard.root + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.frogobox.appkeyboard.model.KeyboardFeatureModel + +/** + * Modern Jetpack Compose Feature Header Bar replacing legacy RecyclerView and ItemKeyboardHeaderBinding. + * Renders horizontally scrollable feature shortcuts according to Material 3 tokens and Angel's design specification. + */ +@Composable +fun KeyboardFeatureHeader( + features: List, + onFeatureClick: (KeyboardFeatureModel) -> Unit, + modifier: Modifier = Modifier +) { + if (features.isEmpty()) return + + Column( + modifier = modifier + .fillMaxWidth() + .height(56.dp) + .background(MaterialTheme.colorScheme.surface) + ) { + LazyRow( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + horizontalArrangement = Arrangement.spacedBy(4.dp), + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically + ) { + items( + items = features, + key = { it.id } + ) { feature -> + KeyboardFeatureHeaderItem( + feature = feature, + onClick = { onFeatureClick(feature) } + ) + } + } + + HorizontalDivider( + thickness = 0.8.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.25f) + ) + } +} + +@Composable +private fun KeyboardFeatureHeaderItem( + feature: KeyboardFeatureModel, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .defaultMinSize(minWidth = 72.dp) + .height(48.dp) + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onClick) + .padding(horizontal = 4.dp, vertical = 2.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = feature.icon), + contentDescription = feature.text, + modifier = Modifier.size(24.dp) + ) + + Text( + text = feature.text, + fontSize = 10.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 2.dp) + ) + } +} diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardImeRootScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardImeRootScreen.kt new file mode 100644 index 0000000..37e2a9f --- /dev/null +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardImeRootScreen.kt @@ -0,0 +1,253 @@ +package com.frogobox.appkeyboard.ui.keyboard.root + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import com.frogobox.appkeyboard.model.AutoTextEntity +import com.frogobox.appkeyboard.model.KeyboardFeatureModel +import com.frogobox.appkeyboard.model.KeyboardFeatureType +import com.frogobox.appkeyboard.model.ThemeType +import com.frogobox.appkeyboard.suggestion.SuggestionResult +import com.frogobox.appkeyboard.ui.keyboard.autotext.AutoTextKeyboardScreen +import com.frogobox.appkeyboard.ui.keyboard.form.FormKeyboardScreen +import com.frogobox.appkeyboard.ui.keyboard.movie.MovieKeyboardScreen +import com.frogobox.appkeyboard.ui.keyboard.news.NewsKeyboardScreen +import com.frogobox.appkeyboard.ui.keyboard.templatetext.TemplateTextKeyboardScreen +import com.frogobox.appkeyboard.ui.keyboard.webview.WebviewKeyboardScreen +import com.frogobox.coreutil.movie.model.TrendingMovie +import com.frogobox.coreutil.news.model.Article +import com.frogobox.libkeyboard.ui.emoji.EmojiCategory +import com.frogobox.libkeyboard.ui.emoji.EmojiCategoryType +import com.frogobox.libkeyboard.ui.emoji.EmojiKeyboardScreen +import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard +import com.frogobox.libkeyboard.ui.main.MainKeyboard +import com.frogobox.libkeyboard.ui.main.MainKeyboardView +import com.frogobox.libkeyboard.ui.main.OnKeyboardActionListener + +/** + * Root composable for KeyboardIME unifying the entire keyboard window: + * - Dynamic background rendering (Solid color vs Scrimmed wallpaper image) + * - Animated Top Bar transitioning between [KeyboardFeatureHeader] and [KeyboardSuggestionBar] (180ms) + * - Smooth [Crossfade] (180ms) panel navigation across QWERTY and feature overlay screens + */ +@Composable +fun KeyboardImeRootScreen( + activePanelState: KeyboardPanelState, + themeType: ThemeType, + themeBackgroundRes: Int, + features: List, + onFeatureClick: (KeyboardFeatureModel) -> Unit, + isSuggestionVisible: Boolean, + suggestionResult: SuggestionResult, + onCandidateSelected: (String, CandidateType) -> Unit, + onSwitchSuggestionMenu: () -> Unit, + onCloseSuggestion: () -> Unit, + currentKeyboard: ItemMainKeyboard?, + onKeyboardActionListener: OnKeyboardActionListener?, + onMainKeyboardInit: (MainKeyboard) -> Unit, + autoTextList: List, + onManageAutoText: () -> Unit, + newsArticles: List
, + isNewsLoading: Boolean, + movieList: List, + isMovieLoading: Boolean, + emojis: List, + selectedEmojiCategory: EmojiCategoryType, + emojiCategories: List, + isEmojiLoading: Boolean, + onSelectEmojiCategory: (EmojiCategoryType) -> Unit, + onEmojiClicked: (String) -> Unit, + onCommitText: (String) -> Unit, + onBackToMain: () -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + // 1. Background Layer (Adaptive Color or Image with Scrim) + if (themeType == ThemeType.IMAGE) { + Image( + painter = painterResource(id = themeBackgroundRes), + contentDescription = null, + modifier = Modifier.matchParentSize(), + contentScale = ContentScale.Crop + ) + Box( + modifier = Modifier + .matchParentSize() + .background(Color.Black.copy(alpha = 0.25f)) + ) + } else { + val context = LocalContext.current + val backgroundColor = remember(themeBackgroundRes) { + try { + Color(ContextCompat.getColor(context, themeBackgroundRes)) + } catch (_: Exception) { + Color(themeBackgroundRes) + } + } + Box( + modifier = Modifier + .matchParentSize() + .background(backgroundColor) + ) + } + + // 2. Content Column Layer + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + // Top Bar Area: Animated transition between Feature Header and Candidate Strip + // Rendered when in MAIN keyboard mode + if (activePanelState == KeyboardPanelState.MAIN) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + ) { + AnimatedContent( + targetState = isSuggestionVisible && suggestionResult.hasSuggestions(), + transitionSpec = { + fadeIn( + animationSpec = tween(durationMillis = 180, easing = FastOutSlowInEasing) + ) togetherWith fadeOut( + animationSpec = tween(durationMillis = 150, easing = FastOutSlowInEasing) + ) + }, + label = "TopBarTransition" + ) { showSuggestions -> + if (showSuggestions) { + KeyboardSuggestionBar( + result = suggestionResult, + onCandidateSelected = onCandidateSelected, + onSwitchMenu = onSwitchSuggestionMenu, + onClose = onCloseSuggestion + ) + } else if (features.isNotEmpty()) { + KeyboardFeatureHeader( + features = features, + onFeatureClick = onFeatureClick + ) + } + } + } + } + + // Main Content Area: Crossfade transition across active keyboard panels + Crossfade( + targetState = activePanelState, + animationSpec = tween(durationMillis = 180, easing = FastOutSlowInEasing), + label = "PanelCrossfade" + ) { panel -> + when (panel) { + KeyboardPanelState.MAIN -> { + MainKeyboardView( + keyboard = currentKeyboard, + onActionListener = onKeyboardActionListener, + onInit = onMainKeyboardInit + ) + } + + KeyboardPanelState.EMOJI -> { + EmojiKeyboardScreen( + emojis = emojis, + selectedCategory = selectedEmojiCategory, + categories = emojiCategories, + onCategorySelected = onSelectEmojiCategory, + onEmojiClicked = onEmojiClicked, + onBackClicked = onBackToMain, + isLoading = isEmojiLoading + ) + } + + KeyboardPanelState.AUTO_TEXT -> { + Box(modifier = Modifier.fillMaxWidth().height(270.dp)) { + AutoTextKeyboardScreen( + autoTextList = autoTextList, + onCommitText = onCommitText, + onBackClick = onBackToMain, + onManageClick = onManageAutoText + ) + } + } + + KeyboardPanelState.TEMPLATE_TEXT_GAME, + KeyboardPanelState.TEMPLATE_TEXT_APP, + KeyboardPanelState.TEMPLATE_TEXT_SALE, + KeyboardPanelState.TEMPLATE_TEXT_LOVE, + KeyboardPanelState.TEMPLATE_TEXT_GREETING -> { + Box(modifier = Modifier.fillMaxWidth().height(270.dp)) { + TemplateTextKeyboardScreen( + initialType = panel.templateFeatureType ?: KeyboardFeatureType.TEMPLATE_TEXT_GAME, + onCommitText = onCommitText, + onBackClick = onBackToMain + ) + } + } + + KeyboardPanelState.NEWS -> { + Box(modifier = Modifier.fillMaxWidth().height(270.dp)) { + NewsKeyboardScreen( + articles = newsArticles, + isLoading = isNewsLoading, + onCommitText = onCommitText, + onBackClick = onBackToMain + ) + } + } + + KeyboardPanelState.MOVIE -> { + Box(modifier = Modifier.fillMaxWidth().height(270.dp)) { + MovieKeyboardScreen( + movieList = movieList, + isLoading = isMovieLoading, + onCommitText = onCommitText, + onBackClick = onBackToMain + ) + } + } + + KeyboardPanelState.WEBVIEW -> { + WebviewKeyboardScreen( + onCommitText = onCommitText, + onBackClick = onBackToMain + ) + } + + KeyboardPanelState.FORM -> { + Box(modifier = Modifier.fillMaxWidth().height(270.dp)) { + FormKeyboardScreen( + onCommitText = onCommitText, + onBackClick = onBackToMain + ) + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardPanelState.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardPanelState.kt new file mode 100644 index 0000000..6585042 --- /dev/null +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardPanelState.kt @@ -0,0 +1,62 @@ +package com.frogobox.appkeyboard.ui.keyboard.root + +import com.frogobox.appkeyboard.model.KeyboardFeatureType + +/** + * Single source of truth reactive state for active keyboard panel in [com.frogobox.appkeyboard.services.KeyboardIME]. + */ +enum class KeyboardPanelState { + MAIN, + EMOJI, + AUTO_TEXT, + TEMPLATE_TEXT_GAME, + TEMPLATE_TEXT_APP, + TEMPLATE_TEXT_SALE, + TEMPLATE_TEXT_LOVE, + TEMPLATE_TEXT_GREETING, + NEWS, + MOVIE, + WEBVIEW, + FORM; + + val isTemplate: Boolean + get() = this in TEMPLATE_STATES + + val templateFeatureType: KeyboardFeatureType? + get() = when (this) { + TEMPLATE_TEXT_GAME -> KeyboardFeatureType.TEMPLATE_TEXT_GAME + TEMPLATE_TEXT_APP -> KeyboardFeatureType.TEMPLATE_TEXT_APP + TEMPLATE_TEXT_SALE -> KeyboardFeatureType.TEMPLATE_TEXT_SALE + TEMPLATE_TEXT_LOVE -> KeyboardFeatureType.TEMPLATE_TEXT_LOVE + TEMPLATE_TEXT_GREETING -> KeyboardFeatureType.TEMPLATE_TEXT_GREETING + else -> null + } + + companion object { + private val TEMPLATE_STATES = setOf( + TEMPLATE_TEXT_GAME, + TEMPLATE_TEXT_APP, + TEMPLATE_TEXT_SALE, + TEMPLATE_TEXT_LOVE, + TEMPLATE_TEXT_GREETING + ) + + fun fromFeature(featureType: KeyboardFeatureType): KeyboardPanelState? { + return when (featureType) { + KeyboardFeatureType.AUTO_TEXT -> AUTO_TEXT + KeyboardFeatureType.TEMPLATE_TEXT_GAME -> TEMPLATE_TEXT_GAME + KeyboardFeatureType.TEMPLATE_TEXT_APP -> TEMPLATE_TEXT_APP + KeyboardFeatureType.TEMPLATE_TEXT_SALE -> TEMPLATE_TEXT_SALE + KeyboardFeatureType.TEMPLATE_TEXT_LOVE -> TEMPLATE_TEXT_LOVE + KeyboardFeatureType.TEMPLATE_TEXT_GREETING -> TEMPLATE_TEXT_GREETING + KeyboardFeatureType.NEWS -> NEWS + KeyboardFeatureType.MOVIE -> MOVIE + KeyboardFeatureType.WEB -> WEBVIEW + KeyboardFeatureType.FORM -> FORM + KeyboardFeatureType.SUGGESTION, + KeyboardFeatureType.CHANGE_KEYBOARD, + KeyboardFeatureType.SETTING -> null + } + } + } +} diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardSuggestionBar.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardSuggestionBar.kt new file mode 100644 index 0000000..c44ad4b --- /dev/null +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardSuggestionBar.kt @@ -0,0 +1,230 @@ +package com.frogobox.appkeyboard.ui.keyboard.root + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.frogobox.appkeyboard.R +import com.frogobox.appkeyboard.suggestion.SuggestionResult + +/** + * Candidate types corresponding to the 3 predictive word strip sections. + */ +enum class CandidateType { + USER_WORD, + PREDICTED_WORD, + AUTOCORRECT_WORD +} + +/** + * Modern Jetpack Compose Candidate Suggestion Bar replacing legacy KeyboardSuggestionView. + * Presents 3 distinct candidate sections (literal user input, hero predicted word, auto-correct alternative) + * along with quick switch and dismiss action controls. + */ +@Composable +fun KeyboardSuggestionBar( + result: SuggestionResult, + onCandidateSelected: (String, CandidateType) -> Unit, + onSwitchMenu: () -> Unit, + onClose: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxWidth() + .height(56.dp) + .background(MaterialTheme.colorScheme.surface) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 4.dp, vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + // Switch Menu Button (Far Left) + Box( + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .clickable(onClick = onSwitchMenu) + .padding(6.dp), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = R.drawable.ic_suggestion_switch), + contentDescription = "Switch to Feature Menu", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp) + ) + } + + Spacer(modifier = Modifier.width(2.dp)) + + // Section 1: User's Word (Literal Raw Input) + val hasUserWord = result.userWord.isNotEmpty() + if (hasUserWord) { + Box( + modifier = Modifier + .weight(1.0f) + .fillMaxHeight() + .clip(RoundedCornerShape(8.dp)) + .clickable { onCandidateSelected(result.userWord, CandidateType.USER_WORD) } + .padding(horizontal = 4.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = "“${result.userWord}”", + fontSize = 13.sp, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.85f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center + ) + } + } else { + Spacer(modifier = Modifier.weight(1.0f)) + } + + // Divider 1 + if (hasUserWord && result.predictedWord.isNotEmpty()) { + VerticalDivider( + modifier = Modifier + .height(20.dp) + .width(1.dp) + .padding(horizontal = 1.dp), + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f) + ) + } + + // Section 2: Predicted Word (Center Hero Candidate Pill) + val hasPredicted = result.predictedWord.isNotEmpty() + if (hasPredicted) { + Surface( + modifier = Modifier + .weight(1.2f) + .height(32.dp) + .clip(RoundedCornerShape(16.dp)) + .clickable { onCandidateSelected(result.predictedWord, CandidateType.PREDICTED_WORD) }, + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.7f), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.35f)), + shadowElevation = 1.dp + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 6.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = result.predictedWord, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center + ) + } + } + } else { + Spacer(modifier = Modifier.weight(1.2f)) + } + + // Divider 2 + val hasAutocorrect = result.autoCorrectWord.isNotEmpty() && + result.autoCorrectWord != result.predictedWord + if (hasAutocorrect && (hasPredicted || hasUserWord)) { + VerticalDivider( + modifier = Modifier + .height(20.dp) + .width(1.dp) + .padding(horizontal = 1.dp), + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f) + ) + } + + // Section 3: Auto-Correct Word (Alternative Fix) + if (hasAutocorrect) { + Box( + modifier = Modifier + .weight(1.0f) + .fillMaxHeight() + .clip(RoundedCornerShape(8.dp)) + .clickable { onCandidateSelected(result.autoCorrectWord, CandidateType.AUTOCORRECT_WORD) } + .padding(horizontal = 4.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = result.autoCorrectWord, + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = Color(0xFF2E7D32), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center + ) + } + } else { + Spacer(modifier = Modifier.weight(1.0f)) + } + + Spacer(modifier = Modifier.width(2.dp)) + + // Close Button (Far Right) + Box( + modifier = Modifier + .size(32.dp) + .clip(CircleShape) + .clickable(onClick = onClose) + .padding(6.dp), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = R.drawable.ic_close_suggestion), + contentDescription = "Clear Suggestions", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(16.dp) + ) + } + } + + HorizontalDivider( + thickness = 0.8.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f) + ) + } +} diff --git a/app/src/test/java/com/frogobox/appkeyboard/KeyboardImeComposeUnitTest.kt b/app/src/test/java/com/frogobox/appkeyboard/KeyboardImeComposeUnitTest.kt new file mode 100644 index 0000000..53ba2f1 --- /dev/null +++ b/app/src/test/java/com/frogobox/appkeyboard/KeyboardImeComposeUnitTest.kt @@ -0,0 +1,160 @@ +package com.frogobox.appkeyboard + +import com.frogobox.appkeyboard.model.KeyboardFeatureType +import com.frogobox.appkeyboard.suggestion.SuggestionResult +import com.frogobox.appkeyboard.ui.keyboard.root.CandidateType +import com.frogobox.appkeyboard.ui.keyboard.root.KeyboardPanelState +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Unit test suite validating TASK-020 Full Pure Compose architecture: + * - KeyboardPanelState state machine transitions & feature type mappings + * - CandidateType enum integrity + * - Suggestion result evaluation for AnimatedContent Top Bar transitions + */ +class KeyboardImeComposeUnitTest { + + @Test + fun testKeyboardPanelStateEnumValues() { + val expectedPanels = listOf( + KeyboardPanelState.MAIN, + KeyboardPanelState.EMOJI, + KeyboardPanelState.AUTO_TEXT, + KeyboardPanelState.TEMPLATE_TEXT_GAME, + KeyboardPanelState.TEMPLATE_TEXT_APP, + KeyboardPanelState.TEMPLATE_TEXT_SALE, + KeyboardPanelState.TEMPLATE_TEXT_LOVE, + KeyboardPanelState.TEMPLATE_TEXT_GREETING, + KeyboardPanelState.NEWS, + KeyboardPanelState.MOVIE, + KeyboardPanelState.WEBVIEW, + KeyboardPanelState.FORM + ) + + assertEquals(12, KeyboardPanelState.entries.size) + expectedPanels.forEach { panel -> + assertTrue(KeyboardPanelState.entries.contains(panel)) + } + } + + @Test + fun testKeyboardPanelStateFromFeatureMapping() { + assertEquals(KeyboardPanelState.AUTO_TEXT, KeyboardPanelState.fromFeature(KeyboardFeatureType.AUTO_TEXT)) + assertEquals(KeyboardPanelState.TEMPLATE_TEXT_GAME, KeyboardPanelState.fromFeature(KeyboardFeatureType.TEMPLATE_TEXT_GAME)) + assertEquals(KeyboardPanelState.TEMPLATE_TEXT_APP, KeyboardPanelState.fromFeature(KeyboardFeatureType.TEMPLATE_TEXT_APP)) + assertEquals(KeyboardPanelState.TEMPLATE_TEXT_SALE, KeyboardPanelState.fromFeature(KeyboardFeatureType.TEMPLATE_TEXT_SALE)) + assertEquals(KeyboardPanelState.TEMPLATE_TEXT_LOVE, KeyboardPanelState.fromFeature(KeyboardFeatureType.TEMPLATE_TEXT_LOVE)) + assertEquals(KeyboardPanelState.TEMPLATE_TEXT_GREETING, KeyboardPanelState.fromFeature(KeyboardFeatureType.TEMPLATE_TEXT_GREETING)) + assertEquals(KeyboardPanelState.NEWS, KeyboardPanelState.fromFeature(KeyboardFeatureType.NEWS)) + assertEquals(KeyboardPanelState.MOVIE, KeyboardPanelState.fromFeature(KeyboardFeatureType.MOVIE)) + assertEquals(KeyboardPanelState.WEBVIEW, KeyboardPanelState.fromFeature(KeyboardFeatureType.WEB)) + assertEquals(KeyboardPanelState.FORM, KeyboardPanelState.fromFeature(KeyboardFeatureType.FORM)) + + // Non-panel utility actions should map to null + assertNull(KeyboardPanelState.fromFeature(KeyboardFeatureType.SUGGESTION)) + assertNull(KeyboardPanelState.fromFeature(KeyboardFeatureType.CHANGE_KEYBOARD)) + assertNull(KeyboardPanelState.fromFeature(KeyboardFeatureType.SETTING)) + } + + @Test + fun testTemplateStateProperties() { + val templatePanels = listOf( + KeyboardPanelState.TEMPLATE_TEXT_GAME, + KeyboardPanelState.TEMPLATE_TEXT_APP, + KeyboardPanelState.TEMPLATE_TEXT_SALE, + KeyboardPanelState.TEMPLATE_TEXT_LOVE, + KeyboardPanelState.TEMPLATE_TEXT_GREETING + ) + + templatePanels.forEach { panel -> + assertTrue(panel.isTemplate) + assertNotNull(panel.templateFeatureType) + } + + assertFalse(KeyboardPanelState.MAIN.isTemplate) + assertFalse(KeyboardPanelState.EMOJI.isTemplate) + assertFalse(KeyboardPanelState.AUTO_TEXT.isTemplate) + assertFalse(KeyboardPanelState.NEWS.isTemplate) + assertFalse(KeyboardPanelState.MOVIE.isTemplate) + assertFalse(KeyboardPanelState.WEBVIEW.isTemplate) + assertFalse(KeyboardPanelState.FORM.isTemplate) + + assertNull(KeyboardPanelState.MAIN.templateFeatureType) + assertNull(KeyboardPanelState.EMOJI.templateFeatureType) + assertNull(KeyboardPanelState.AUTO_TEXT.templateFeatureType) + assertNull(KeyboardPanelState.NEWS.templateFeatureType) + } + + @Test + fun testCandidateTypeEnumIntegrity() { + assertEquals(3, CandidateType.entries.size) + assertEquals(CandidateType.USER_WORD, CandidateType.valueOf("USER_WORD")) + assertEquals(CandidateType.PREDICTED_WORD, CandidateType.valueOf("PREDICTED_WORD")) + assertEquals(CandidateType.AUTOCORRECT_WORD, CandidateType.valueOf("AUTOCORRECT_WORD")) + } + + @Test + fun testTopBarTransitionConditionLogic() { + // When suggestions are hidden or empty -> Header toolbar displayed + val emptyResult = SuggestionResult.EMPTY + val isSuggestionVisibleFalse = false + val condition1 = isSuggestionVisibleFalse && emptyResult.hasSuggestions() + assertFalse(condition1) + + // When suggestions are visible and candidates exist -> Candidate bar displayed + val populatedResult = SuggestionResult( + userWord = "rec", + predictedWord = "receive", + autoCorrectWord = "record" + ) + val isSuggestionVisibleTrue = true + val condition2 = isSuggestionVisibleTrue && populatedResult.hasSuggestions() + assertTrue(condition2) + + // When dismissed -> immediate return to Header + val dismissedResult = SuggestionResult.EMPTY + val condition3 = isSuggestionVisibleTrue && dismissedResult.hasSuggestions() + assertFalse(condition3) + } + + @Test + fun testWordBeforeCursorExtractionLogic() { + fun extractWord(text: String): String { + return text.takeLastWhile { it.isLetterOrDigit() || it == '\'' } + } + + assertEquals("hello", extractWord("say hello")) + assertEquals("world", extractWord("hello world")) + assertEquals("don't", extractWord("please don't")) + assertEquals("", extractWord("hello ")) + assertEquals("", extractWord("")) + assertEquals("123", extractWord("order 123")) + } + + @Test + fun testFeatureHeaderIconAndTextIntegrity() { + val features = KeyboardFeatureType.entries.map { it.mapToModel() } + features.forEach { feature -> + assertTrue("Feature text must not be empty", feature.text.isNotEmpty()) + assertTrue("Feature icon resource must be valid drawable", feature.icon != 0) + } + } + + @Test + fun testKeyboardTopBarDimensionsConsistency() { + val standardTopBarHeightDp = 56 + val featureItemMinHeightDp = 48 + val featureItemMinWidthDp = 72 + val featureIconSizeDp = 24 + + assertTrue("Feature item height must comfortably fit within Top Bar", featureItemMinHeightDp < standardTopBarHeightDp) + assertTrue("Icon size must fit within feature item", featureIconSizeDp < featureItemMinHeightDp) + assertTrue("Min width must be at least 72dp to avoid multi-line label clipping", featureItemMinWidthDp >= 72) + } + +} diff --git a/lib-keyboard/build.gradle.kts b/lib-keyboard/build.gradle.kts index 6353419..4425189 100644 --- a/lib-keyboard/build.gradle.kts +++ b/lib-keyboard/build.gradle.kts @@ -41,6 +41,12 @@ android { targetCompatibility = JavaVersion.VERSION_17 } + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } + publishing { singleVariant("release") { withSourcesJar() @@ -78,6 +84,8 @@ dependencies { androidTestImplementation(composeBom) testImplementation(libs.junit) + testImplementation("org.robolectric:robolectric:4.16.1") + testImplementation("androidx.test:core:1.7.0") debugImplementation(libs.androidx.compose.ui.tooling) diff --git a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboardIME.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboardIME.kt index 0076ae8..a1e9c8c 100644 --- a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboardIME.kt +++ b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/common/core/BaseKeyboardIME.kt @@ -25,7 +25,7 @@ import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard.Companion.SHIFT_ON_PERM import com.frogobox.libkeyboard.ui.main.OnKeyboardActionListener // based on https://www.androidauthority.com/lets-build-custom-keyboard-android-832362/ -abstract class BaseKeyboardIME : InputMethodService(), OnKeyboardActionListener, IKeyboardIME { +abstract class BaseKeyboardIME : InputMethodService(), OnKeyboardActionListener, IKeyboardIME { companion object { // How quickly do we have to doubletap shift to enable permanent caps lock @@ -47,9 +47,9 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo var enterKeyType = IME_ACTION_NONE var switchToLetters = false - var binding: VB? = null - - abstract fun setupViewBinding() : VB + open fun setupInputView(): View { + return View(this) + } override fun onCreate() { setTheme(R.style.Theme_Research) @@ -73,11 +73,11 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo } override fun onCreateInputView(): View { - binding = setupViewBinding() + val view = setupInputView() setupBinding() initCurrentInputConnection() initView() - return binding!!.root + return view } override fun onPress(primaryCode: Int) { @@ -442,4 +442,21 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo return R.xml.keys_letters_qwerty } +} + +/** + * Legacy ViewBinding adapter base class for consumers that require [ViewBinding]. + */ +abstract class BaseViewBindingKeyboardIME : BaseKeyboardIME() { + + var binding: VB? = null + + abstract fun setupViewBinding(): VB + + override fun setupInputView(): View { + val vb = setupViewBinding() + binding = vb + return vb.root + } + } \ No newline at end of file diff --git a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt index 82571dd..567ef3e 100644 --- a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt +++ b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt @@ -194,20 +194,20 @@ class MainKeyboard @JvmOverloads constructor( private val LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout() } - // handle system default theme (Material You) specially as the color is taken from the system, not hardcoded by us - init { + val defaultKeyTextSize = resources.getDimensionPixelSize(R.dimen.keyboard_text_size) + mKeyTextSize = defaultKeyTextSize + val attributes = context.obtainStyledAttributes(attrs, R.styleable.FrogoKeyboardView, 0, defStyleRes) val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - val keyTextSize = 0 val indexCnt = attributes.indexCount try { for (i in 0 until indexCnt) { when (val attr = attributes.getIndex(i)) { R.styleable.FrogoKeyboardView_keyTextSize -> mKeyTextSize = - attributes.getDimensionPixelSize(attr, 18) + attributes.getDimensionPixelSize(attr, defaultKeyTextSize) } } } finally { @@ -217,6 +217,10 @@ class MainKeyboard @JvmOverloads constructor( mTextColor = context.getColorExt(R.color.keypad_text) mBackgroundColor = context.getColorExt(R.color.keyboard_board) + if (background == null) { + setBackgroundColor(mBackgroundColor) + } + mPopupLayout = R.layout.keyboard_main_mini mVerticalCorrection = resources.getDimension(R.dimen.vertical_correction).toInt() mLabelTextSize = resources.getDimension(R.dimen.label_text_size).toInt() @@ -238,7 +242,7 @@ class MainKeyboard @JvmOverloads constructor( mPaint = Paint().apply { isAntiAlias = true - textSize = keyTextSize.toFloat() + textSize = mKeyTextSize.toFloat() textAlign = Align.CENTER alpha = 255 } @@ -255,6 +259,8 @@ class MainKeyboard @JvmOverloads constructor( initCachedDrawables() } + fun getKeyTextSize(): Int = mKeyTextSize + fun initCachedDrawables() { mKeypadDefaultDrawable = resources.getDrawable(R.drawable.keypad_default, context.theme) mKeypadActionDrawable = resources.getDrawable(R.drawable.keypad_action, context.theme) @@ -283,7 +289,7 @@ class MainKeyboard @JvmOverloads constructor( } - override fun onVisibilityChanged(changedView: View, visibility: Int) { + public override fun onVisibilityChanged(changedView: View, visibility: Int) { super.onVisibilityChanged(changedView, visibility) if (visibility == VISIBLE) { @@ -295,14 +301,21 @@ class MainKeyboard @JvmOverloads constructor( val miniKeyboardBackgroundColor = context.getColorExt(R.color.keypad) if (changedView == findViewById(R.id.mini_keyboard_view)) { - val previewBackground = background as LayerDrawable - previewBackground.findDrawableByLayerId(R.id.button_background_shape) - .applyColorFilter(miniKeyboardBackgroundColor) - previewBackground.findDrawableByLayerId(R.id.button_background_stroke) - .applyColorFilter(strokeColor) - background = previewBackground + val previewBackground = background as? LayerDrawable + previewBackground?.findDrawableByLayerId(R.id.button_background_shape) + ?.applyColorFilter(miniKeyboardBackgroundColor) + previewBackground?.findDrawableByLayerId(R.id.button_background_stroke) + ?.applyColorFilter(strokeColor) + if (previewBackground != null) { + background = previewBackground + } } else { - background.applyColorFilter(mBackgroundColor) + val currentBg = background + if (currentBg != null) { + currentBg.applyColorFilter(mBackgroundColor) + } else { + setBackgroundColor(mBackgroundColor) + } } } diff --git a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboardScreen.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboardScreen.kt index 032a9fa..c2eb3e6 100644 --- a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboardScreen.kt +++ b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboardScreen.kt @@ -26,11 +26,13 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView +import com.frogobox.libkeyboard.R import com.frogobox.libkeyboard.ui.theme.FrogoLibKeyboardTheme import com.frogobox.libkeyboard.ui.theme.KeypadActionDark import com.frogobox.libkeyboard.ui.theme.KeypadActionLight import com.frogobox.libkeyboard.ui.theme.KeypadDark import com.frogobox.libkeyboard.ui.theme.KeypadLight +import com.frogobox.sdk.ext.getColorExt /** * Jetpack Compose interoperability wrapper for hosting [MainKeyboard] within Compose UI trees. @@ -46,6 +48,9 @@ fun MainKeyboardView( modifier = modifier, factory = { context -> MainKeyboard(context, null).apply { + if (background == null) { + setBackgroundColor(context.getColorExt(R.color.keyboard_board)) + } this.mOnKeyboardActionListener = onActionListener keyboard?.let { setKeyboard(it) } onInit(this) diff --git a/lib-keyboard/src/test/java/com/frogobox/libkeyboard/MainKeyboardBackgroundTest.kt b/lib-keyboard/src/test/java/com/frogobox/libkeyboard/MainKeyboardBackgroundTest.kt new file mode 100644 index 0000000..af494d2 --- /dev/null +++ b/lib-keyboard/src/test/java/com/frogobox/libkeyboard/MainKeyboardBackgroundTest.kt @@ -0,0 +1,119 @@ +package com.frogobox.libkeyboard + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.LayerDrawable +import android.view.View +import androidx.test.core.app.ApplicationProvider +import com.frogobox.libkeyboard.ui.main.MainKeyboard +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Unit and Robolectric regression test suite for TASK-021 & TASK-022: + * Validates defensive null-safety architecture and valid keyTextSize initialization + * during programmatic instantiation and window attachment / visibility changes. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34]) +class MainKeyboardBackgroundTest { + + private lateinit var context: Context + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + } + + @Test + fun testProgrammaticInstantiation_initializesValidKeyTextSize() { + // Given programmatic instantiation with null AttributeSet (Compose interop pattern) + val keyboard = MainKeyboard(context, null) + + // Then keyTextSize must be initialized to keyboard_text_size dimension instead of 0 + val expectedSize = context.resources.getDimensionPixelSize(R.dimen.keyboard_text_size) + assertTrue("Key text size must be greater than 0", keyboard.getKeyTextSize() > 0) + assertEquals("Key text size should match R.dimen.keyboard_text_size", expectedSize, keyboard.getKeyTextSize()) + } + + @Test + fun testProgrammaticInstantiation_initializesNonNullBackground() { + // Given programmatic instantiation with null AttributeSet (Compose interop pattern) + val keyboard = MainKeyboard(context, null) + + // Then background must be non-null immediately upon init (Defense Layer 1) + assertNotNull("Background must be non-null after programmatic instantiation", keyboard.background) + assertTrue("Background should be an instance of ColorDrawable", keyboard.background is ColorDrawable) + } + + @Test + fun testOnVisibilityChanged_whenBackgroundNull_fallbacksGracefullyWithoutNPE() { + // Given programmatic instantiation + val keyboard = MainKeyboard(context, null) + + // Simulate edge-case where background was cleared or set to null at runtime + keyboard.background = null + assertNull(keyboard.background) + + // When onVisibilityChanged is triggered with VISIBLE (as in dispatchAttachedToWindow) + keyboard.onVisibilityChanged(keyboard, View.VISIBLE) + + // Then no NPE must be thrown, and background must be defensively restored (Defense Layer 2) + assertNotNull("Background must be safely restored upon becoming visible", keyboard.background) + assertTrue("Restored background should be a ColorDrawable", keyboard.background is ColorDrawable) + } + + @Test + fun testOnVisibilityChanged_whenBackgroundNonNull_appliesColorFilterSafely() { + // Given programmatic instantiation + val keyboard = MainKeyboard(context, null) + assertNotNull(keyboard.background) + + // When onVisibilityChanged is dispatched with VISIBLE + keyboard.onVisibilityChanged(keyboard, View.VISIBLE) + + // Then background remains valid and non-null without exception + assertNotNull("Background must remain non-null after visibility change", keyboard.background) + } + + @Test + fun testOnVisibilityChanged_miniKeyboardView_safeCastWithoutClassCastException() { + // Given programmatic instantiation + val keyboard = MainKeyboard(context, null) + + // Set background to a non-LayerDrawable (e.g., ColorDrawable) + keyboard.background = ColorDrawable(Color.DKGRAY) + + // Create a simulated mini_keyboard_view + val miniView = View(context).apply { + id = R.id.mini_keyboard_view + } + + // When onVisibilityChanged is triggered targeting mini_keyboard_view + // with a non-LayerDrawable background + keyboard.onVisibilityChanged(miniView, View.VISIBLE) + + // Then no ClassCastException or NPE must occur (safe-cast as? LayerDrawable succeeds) + assertNotNull("Background remains safe and non-null", keyboard.background) + } + + @Test + fun testOnVisibilityChanged_whenVisibilityNotVisible_doesNotThrow() { + val keyboard = MainKeyboard(context, null) + keyboard.background = null + + // GONE or INVISIBLE should bypass visibility logic cleanly + keyboard.onVisibilityChanged(keyboard, View.GONE) + keyboard.onVisibilityChanged(keyboard, View.INVISIBLE) + + assertNull("Background remains null when view is not visible", keyboard.background) + } +} From 3cd63191bf39db4d3c57182df56b61729dda45ea Mon Sep 17 00:00:00 2001 From: amirisback Date: Sun, 20 Sep 2026 15:03:09 +0700 Subject: [PATCH 6/8] feat: Jetpack compose --- .../appkeyboard/services/KeyboardIME.kt | 3 +- .../suggestion/KeyboardSuggestionView.kt | 115 -------- .../ui/keyboard/root/KeyboardSuggestionBar.kt | 2 +- .../res/drawable/bg_feature_header_item.xml | 10 - .../drawable/bg_feature_icon_container.xml | 5 - .../main/res/drawable/bg_suggestion_bar.xml | 13 - .../main/res/drawable/bg_suggestion_pill.xml | 6 - .../main/res/layout/item_keyboard_header.xml | 45 --- app/src/main/res/layout/keyboard_ime.xml | 130 --------- .../res/layout/layout_keyboard_suggestion.xml | 135 --------- app/src/main/res/values-night/color.xml | 12 - app/src/main/res/values/color.xml | 12 - .../libkeyboard/ui/main/KeyPreviewBubble.kt | 100 +++++++ .../libkeyboard/ui/main/MainKeyboard.kt | 276 ++++++++++-------- .../libkeyboard/ui/main/MainKeyboardScreen.kt | 128 +++++--- .../libkeyboard/ui/main/MiniKeyboardPopup.kt | 133 +++++++++ .../res/drawable/ic_keyboard_arrow_down.xml | 10 - .../res/drawable/ic_keyboard_arrow_up.xml | 10 - .../src/main/res/drawable/keypad_emoji.xml | 10 - .../main/res/layout/item_keyboard_emoji.xml | 11 - .../main/res/layout/item_keyboard_main.xml | 10 - .../src/main/res/layout/keyboard_emoji.xml | 72 ----- .../main/res/layout/keyboard_main_mini.xml | 6 - .../src/main/res/values-night/colors.xml | 2 - lib-keyboard/src/main/res/values/colors.xml | 7 - lib-keyboard/src/main/res/values/dimens.xml | 9 - lib-keyboard/src/main/res/values/ids.xml | 4 + lib-keyboard/src/main/res/values/strings.xml | 6 - .../res/xml/keys_letters_english_qwerty.xml | 178 ----------- .../libkeyboard/KeyboardPopupsComposeTest.kt | 77 +++++ 30 files changed, 552 insertions(+), 985 deletions(-) delete mode 100644 app/src/main/java/com/frogobox/appkeyboard/suggestion/KeyboardSuggestionView.kt delete mode 100644 app/src/main/res/drawable/bg_feature_header_item.xml delete mode 100644 app/src/main/res/drawable/bg_feature_icon_container.xml delete mode 100644 app/src/main/res/drawable/bg_suggestion_bar.xml delete mode 100644 app/src/main/res/drawable/bg_suggestion_pill.xml delete mode 100644 app/src/main/res/layout/item_keyboard_header.xml delete mode 100644 app/src/main/res/layout/keyboard_ime.xml delete mode 100644 app/src/main/res/layout/layout_keyboard_suggestion.xml create mode 100644 lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/KeyPreviewBubble.kt create mode 100644 lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MiniKeyboardPopup.kt delete mode 100644 lib-keyboard/src/main/res/drawable/ic_keyboard_arrow_down.xml delete mode 100644 lib-keyboard/src/main/res/drawable/ic_keyboard_arrow_up.xml delete mode 100644 lib-keyboard/src/main/res/drawable/keypad_emoji.xml delete mode 100644 lib-keyboard/src/main/res/layout/item_keyboard_emoji.xml delete mode 100644 lib-keyboard/src/main/res/layout/item_keyboard_main.xml delete mode 100644 lib-keyboard/src/main/res/layout/keyboard_emoji.xml delete mode 100644 lib-keyboard/src/main/res/layout/keyboard_main_mini.xml create mode 100644 lib-keyboard/src/main/res/values/ids.xml delete mode 100644 lib-keyboard/src/main/res/xml/keys_letters_english_qwerty.xml create mode 100644 lib-keyboard/src/test/java/com/frogobox/libkeyboard/KeyboardPopupsComposeTest.kt diff --git a/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt b/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt index 38a484e..b1810f4 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt @@ -61,8 +61,7 @@ import javax.inject.Inject /** * Modern 100% Full Pure Jetpack Compose InputMethodService. - * Replaces legacy ViewBinding (keyboard_ime.xml, item_keyboard_header.xml, layout_keyboard_suggestion.xml) - * with a unified ComposeView, reactive panel navigation, and direct Compose feature integrations. + * Uses a unified ComposeView, reactive panel navigation, and direct Compose feature integrations. */ @AndroidEntryPoint class KeyboardIME : BaseKeyboardIME() { diff --git a/app/src/main/java/com/frogobox/appkeyboard/suggestion/KeyboardSuggestionView.kt b/app/src/main/java/com/frogobox/appkeyboard/suggestion/KeyboardSuggestionView.kt deleted file mode 100644 index c0a90b3..0000000 --- a/app/src/main/java/com/frogobox/appkeyboard/suggestion/KeyboardSuggestionView.kt +++ /dev/null @@ -1,115 +0,0 @@ -package com.frogobox.appkeyboard.suggestion - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import android.widget.FrameLayout -import com.frogobox.appkeyboard.databinding.LayoutKeyboardSuggestionBinding -import com.frogobox.sdk.ext.gone -import com.frogobox.sdk.ext.visible - -/** - * Custom Suggestion Strip View displaying the 3 candidate sections: - * Section 1: User's typed word - * Section 2: Predicted word (central hero) - * Section 3: Auto-correct word - */ -class KeyboardSuggestionView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : FrameLayout(context, attrs, defStyleAttr) { - - val binding: LayoutKeyboardSuggestionBinding = - LayoutKeyboardSuggestionBinding.inflate(LayoutInflater.from(context), this, true) - - private var currentResult: SuggestionResult = SuggestionResult.EMPTY - - var onCandidateSelected: ((selectedWord: String, candidateType: CandidateType) -> Unit)? = null - var onSwitchMenuClicked: (() -> Unit)? = null - var onCloseClicked: (() -> Unit)? = null - - enum class CandidateType { - USER_WORD, - PREDICTED_WORD, - AUTOCORRECT_WORD - } - - init { - setupListeners() - } - - private fun setupListeners() { - binding.sectionUserWord.setOnClickListener { - val word = currentResult.userWord - if (word.isNotEmpty()) { - onCandidateSelected?.invoke(word, CandidateType.USER_WORD) - } - } - - binding.sectionPredictedWord.setOnClickListener { - val word = currentResult.predictedWord - if (word.isNotEmpty()) { - onCandidateSelected?.invoke(word, CandidateType.PREDICTED_WORD) - } - } - - binding.sectionAutocorrectWord.setOnClickListener { - val word = currentResult.autoCorrectWord - if (word.isNotEmpty()) { - onCandidateSelected?.invoke(word, CandidateType.AUTOCORRECT_WORD) - } - } - - binding.btnSwitchMenu.setOnClickListener { - onSwitchMenuClicked?.invoke() - } - - binding.btnClearSuggestion.setOnClickListener { - onCloseClicked?.invoke() - } - } - - /** - * Updates the 3 suggestion candidates in real-time - */ - fun setSuggestions(result: SuggestionResult) { - currentResult = result - - // Section 1: User's literal word - if (result.userWord.isNotEmpty()) { - binding.tvUserWord.text = "“${result.userWord}”" - binding.sectionUserWord.visible() - binding.divider1.visible() - } else { - binding.tvUserWord.text = "" - binding.sectionUserWord.gone() - binding.divider1.gone() - } - - // Section 2: Predicted word - if (result.predictedWord.isNotEmpty()) { - binding.tvPredictedWord.text = result.predictedWord - binding.sectionPredictedWord.visible() - } else { - binding.tvPredictedWord.text = "" - binding.sectionPredictedWord.gone() - } - - // Section 3: Auto-correct word - if (result.autoCorrectWord.isNotEmpty() && result.autoCorrectWord != result.predictedWord) { - binding.tvAutocorrectWord.text = result.autoCorrectWord - binding.sectionAutocorrectWord.visible() - binding.divider2.visible() - } else { - binding.tvAutocorrectWord.text = "" - binding.sectionAutocorrectWord.gone() - binding.divider2.gone() - } - } - - fun clearSuggestions() { - setSuggestions(SuggestionResult.EMPTY) - } - -} diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardSuggestionBar.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardSuggestionBar.kt index c44ad4b..2e60aa4 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardSuggestionBar.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardSuggestionBar.kt @@ -47,7 +47,7 @@ enum class CandidateType { } /** - * Modern Jetpack Compose Candidate Suggestion Bar replacing legacy KeyboardSuggestionView. + * Modern Jetpack Compose Candidate Suggestion Bar. * Presents 3 distinct candidate sections (literal user input, hero predicted word, auto-correct alternative) * along with quick switch and dismiss action controls. */ diff --git a/app/src/main/res/drawable/bg_feature_header_item.xml b/app/src/main/res/drawable/bg_feature_header_item.xml deleted file mode 100644 index 17318c0..0000000 --- a/app/src/main/res/drawable/bg_feature_header_item.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/bg_feature_icon_container.xml b/app/src/main/res/drawable/bg_feature_icon_container.xml deleted file mode 100644 index 7e5bca2..0000000 --- a/app/src/main/res/drawable/bg_feature_icon_container.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/bg_suggestion_bar.xml b/app/src/main/res/drawable/bg_suggestion_bar.xml deleted file mode 100644 index 5736f68..0000000 --- a/app/src/main/res/drawable/bg_suggestion_bar.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/bg_suggestion_pill.xml b/app/src/main/res/drawable/bg_suggestion_pill.xml deleted file mode 100644 index 6af3416..0000000 --- a/app/src/main/res/drawable/bg_suggestion_pill.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/app/src/main/res/layout/item_keyboard_header.xml b/app/src/main/res/layout/item_keyboard_header.xml deleted file mode 100644 index d824049..0000000 --- a/app/src/main/res/layout/item_keyboard_header.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/keyboard_ime.xml b/app/src/main/res/layout/keyboard_ime.xml deleted file mode 100644 index 309430c..0000000 --- a/app/src/main/res/layout/keyboard_ime.xml +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/layout_keyboard_suggestion.xml b/app/src/main/res/layout/layout_keyboard_suggestion.xml deleted file mode 100644 index 4bb0f18..0000000 --- a/app/src/main/res/layout/layout_keyboard_suggestion.xml +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values-night/color.xml b/app/src/main/res/values-night/color.xml index d84e03b..c3b87e0 100644 --- a/app/src/main/res/values-night/color.xml +++ b/app/src/main/res/values-night/color.xml @@ -12,16 +12,4 @@ #D81B60 #FDD835 - - #1E232A - #B0BEC5 - #4FC3F7 - #0D47A1 - #ECEFF1 - #37474F - - - #21262D - #F0F6FC - \ No newline at end of file diff --git a/app/src/main/res/values/color.xml b/app/src/main/res/values/color.xml index b50e7dc..1746e8e 100644 --- a/app/src/main/res/values/color.xml +++ b/app/src/main/res/values/color.xml @@ -12,16 +12,4 @@ #C2185B #FBC02D - - #F4F6F8 - #546E7A - #0288D1 - #E1F5FE - #37474F - #E0E0E0 - - - #0D000000 - @color/tv_text - \ No newline at end of file diff --git a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/KeyPreviewBubble.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/KeyPreviewBubble.kt new file mode 100644 index 0000000..a27bac8 --- /dev/null +++ b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/KeyPreviewBubble.kt @@ -0,0 +1,100 @@ +package com.frogobox.libkeyboard.ui.main + +import android.graphics.drawable.Drawable +import android.widget.ImageView +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import com.frogobox.libkeyboard.ui.theme.FrogoLibKeyboardTheme +import com.frogobox.libkeyboard.ui.theme.KeypadDark +import com.frogobox.libkeyboard.ui.theme.KeypadLight + +/** + * Modern Jetpack Compose Key Preview Bubble component replacing legacy item_keyboard_main.xml. + * Renders a floating elevated tactile preview above the pressed key. + */ +@Composable +fun KeyPreviewBubble( + label: String, + modifier: Modifier = Modifier, + icon: Drawable? = null, + isDarkTheme: Boolean = false +) { + val backgroundColor = if (isDarkTheme) KeypadDark else KeypadLight + val contentColor = if (isDarkTheme) Color.White else Color(0xFF1F1F1F) + + Surface( + modifier = modifier + .wrapContentSize() + .defaultMinSize(minWidth = 48.dp, minHeight = 56.dp), + shape = RoundedCornerShape(10.dp), + color = backgroundColor, + shadowElevation = 6.dp, + tonalElevation = 2.dp + ) { + Box( + modifier = Modifier + .wrapContentSize() + .padding(horizontal = 12.dp, vertical = 8.dp), + contentAlignment = Alignment.Center + ) { + if (icon != null) { + AndroidView( + modifier = Modifier.size(28.dp), + factory = { context -> + ImageView(context).apply { + setImageDrawable(icon) + scaleType = ImageView.ScaleType.FIT_CENTER + } + }, + update = { imageView -> + imageView.setImageDrawable(icon) + } + ) + } else { + Text( + text = label, + fontSize = if (label.length > 2) 16.sp else 24.sp, + fontWeight = FontWeight.Bold, + color = contentColor, + textAlign = TextAlign.Center + ) + } + } + } +} + +@Preview(name = "Key Preview Light", showBackground = true) +@Composable +private fun KeyPreviewBubbleLightPreview() { + FrogoLibKeyboardTheme(darkTheme = false) { + Box(modifier = Modifier.padding(16.dp)) { + KeyPreviewBubble(label = "A", isDarkTheme = false) + } + } +} + +@Preview(name = "Key Preview Dark", showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun KeyPreviewBubbleDarkPreview() { + FrogoLibKeyboardTheme(darkTheme = true) { + Box(modifier = Modifier.padding(16.dp)) { + KeyPreviewBubble(label = "K", isDarkTheme = true) + } + } +} diff --git a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt index 567ef3e..97641cc 100644 --- a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt +++ b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboard.kt @@ -19,10 +19,10 @@ import android.util.AttributeSet import android.util.TypedValue import android.view.Gravity import android.view.HapticFeedbackConstants -import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.ViewConfiguration +import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityManager import android.widget.PopupWindow @@ -54,6 +54,8 @@ import java.util.Locale import kotlin.math.floor import kotlin.math.max import kotlin.math.min +import androidx.core.graphics.createBitmap +import androidx.core.graphics.withSave @SuppressLint("UseCompatLoadingForDrawables", "ClickableViewAccessibility") class MainKeyboard @JvmOverloads constructor( @@ -114,7 +116,6 @@ class MainKeyboard @JvmOverloads constructor( private var mPopupX = 0 private var mPopupY = 0 private var mRepeatKeyIndex = NOT_A_KEY - private var mPopupLayout = 0 private var mAbortKey = false private var mIsLongPressingSpace = false private var mLastSpaceMoveX = 0 @@ -200,7 +201,6 @@ class MainKeyboard @JvmOverloads constructor( val attributes = context.obtainStyledAttributes(attrs, R.styleable.FrogoKeyboardView, 0, defStyleRes) - val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater val indexCnt = attributes.indexCount try { @@ -221,14 +221,13 @@ class MainKeyboard @JvmOverloads constructor( setBackgroundColor(mBackgroundColor) } - mPopupLayout = R.layout.keyboard_main_mini mVerticalCorrection = resources.getDimension(R.dimen.vertical_correction).toInt() mLabelTextSize = resources.getDimension(R.dimen.label_text_size).toInt() mPreviewHeight = resources.getDimension(R.dimen.key_height).toInt() mSpaceMoveThreshold = resources.getDimension(com.frogobox.ui.R.dimen.frogo_dimen_8dp).toInt() mDeleteSwipeThreshold = (resources.getDimension(R.dimen.key_height) * 0.6f).toInt() - mPreviewText = inflater.inflate(resources.getLayout(R.layout.item_keyboard_main), null) as TextView + mPreviewText = createPreviewTextView() mPreviewTextSizeLarge = resources.getDimension(R.dimen.preview_text_size).toInt() mPreviewPopup = PopupWindow(context) @@ -261,6 +260,36 @@ class MainKeyboard @JvmOverloads constructor( fun getKeyTextSize(): Int = mKeyTextSize + fun setKeyTextSize(size: Int) { + mKeyTextSize = size + mPaint.textSize = size.toFloat() + } + + private fun createPreviewTextView(): TextView { + val padding = resources.getDimensionPixelSize(R.dimen.key_margin) + return TextView(context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + setBackgroundResource(R.drawable.keypad_pop_up) + gravity = Gravity.CENTER + includeFontPadding = false + setPadding(padding, padding, padding, padding) + setTextColor(context.getColorExt(R.color.keypad_text)) + typeface = Typeface.DEFAULT_BOLD + setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.preview_text_size)) + } + } + + private fun createMiniKeyboard(): MainKeyboard { + return MainKeyboard(context, null).apply { + id = R.id.mini_keyboard_view + setBackgroundResource(R.drawable.keypad_pop_up) + setKeyTextSize(resources.getDimensionPixelSize(R.dimen.preview_text_size)) + } + } + fun initCachedDrawables() { mKeypadDefaultDrawable = resources.getDrawable(R.drawable.keypad_default, context.theme) mKeypadActionDrawable = resources.getDrawable(R.drawable.keypad_action, context.theme) @@ -446,11 +475,11 @@ class MainKeyboard @JvmOverloads constructor( @SuppressLint("UseCompatLoadingForDrawables") private fun onBufferDraw() { if (mBuffer == null || mKeyboardChanged) { - if (mBuffer == null || mKeyboardChanged && (mBuffer!!.width != width || mBuffer!!.height != height)) { + if (mBuffer == null || (mBuffer!!.width != width || mBuffer!!.height != height)) { // Make sure our bitmap is at least 1x1 val width = max(1, width) val height = max(1, height) - mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + mBuffer = createBitmap(width, height) mCanvas = Canvas(mBuffer!!) } invalidateAllKeys() @@ -461,123 +490,133 @@ class MainKeyboard @JvmOverloads constructor( return } - mCanvas!!.save() - val canvas = mCanvas - canvas!!.clipRect(mDirtyRect) - val paint = mPaint - val keys = mKeys - paint.color = mTextColor - // Reuse class-level paint instead of allocating each frame - mSmallLetterPaint.apply { - set(paint) - color = paint.color.adjustAlpha(0.8f) - textSize = mTopSmallNumberSize - typeface = Typeface.DEFAULT - } - val smallLetterPaint = mSmallLetterPaint - - canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) + mCanvas!!.withSave { + val canvas = mCanvas + canvas!!.clipRect(mDirtyRect) + val paint = mPaint + val keys = mKeys + paint.color = mTextColor + // Reuse class-level paint instead of allocating each frame + mSmallLetterPaint.apply { + set(paint) + color = paint.color.adjustAlpha(0.8f) + textSize = mTopSmallNumberSize + typeface = Typeface.DEFAULT + } + val smallLetterPaint = mSmallLetterPaint - val keyCount = keys.size - val kb = mKeyboard - val defaultBg = mKeypadDefaultDrawable ?: resources.getDrawable(R.drawable.keypad_default, context.theme) - val actionBg = mKeypadActionDrawable ?: resources.getDrawable(R.drawable.keypad_action, context.theme) + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) - for (i in 0 until keyCount) { - val key = keys[i] - val code = key.code - val keyBackground = when (code) { - KEYCODE_SHIFT, - KEYCODE_DELETE, - KEYCODE_ENTER, - KEYCODE_MODE_CHANGE, - KEYCODE_TAB, - KEYCODE_ARROW_LEFT, - KEYCODE_ARROW_RIGHT, - KEYCODE_ARROW_UP, - KEYCODE_ARROW_DOWN -> actionBg - else -> defaultBg - } + val keyCount = keys.size + val kb = mKeyboard + val defaultBg = mKeypadDefaultDrawable ?: resources.getDrawable( + R.drawable.keypad_default, + context.theme + ) + val actionBg = mKeypadActionDrawable ?: resources.getDrawable( + R.drawable.keypad_action, + context.theme + ) - // Switch the character to uppercase if shift is pressed - val label = adjustCase(key.label)?.toString() - val bounds = keyBackground.bounds - if (key.width != bounds.right || key.height != bounds.bottom) { - keyBackground.setBounds(0, 0, key.width, key.height) - } + for (i in 0 until keyCount) { + val key = keys[i] + val code = key.code + val keyBackground = when (code) { + KEYCODE_SHIFT, + KEYCODE_DELETE, + KEYCODE_ENTER, + KEYCODE_MODE_CHANGE, + KEYCODE_TAB, + KEYCODE_ARROW_LEFT, + KEYCODE_ARROW_RIGHT, + KEYCODE_ARROW_UP, + KEYCODE_ARROW_DOWN, + -> actionBg + + else -> defaultBg + } - keyBackground.state = when { - key.pressed -> intArrayOf(android.R.attr.state_pressed) - key.focused -> intArrayOf(android.R.attr.state_focused) - else -> intArrayOf() - } + // Switch the character to uppercase if shift is pressed + val label = adjustCase(key.label)?.toString() + val bounds = keyBackground.bounds + if (key.width != bounds.right || key.height != bounds.bottom) { + keyBackground.setBounds(0, 0, key.width, key.height) + } - canvas.translate(key.x.toFloat(), key.y.toFloat()) - keyBackground.draw(canvas) - if (label?.isNotEmpty() == true) { - // For characters, use large font. For labels like "Done", use small font. - if (label.length > 1) { - paint.textSize = mLabelTextSize.toFloat() - paint.typeface = Typeface.DEFAULT_BOLD - } else { - paint.textSize = mKeyTextSize.toFloat() - paint.typeface = Typeface.DEFAULT + keyBackground.state = when { + key.pressed -> intArrayOf(android.R.attr.state_pressed) + key.focused -> intArrayOf(android.R.attr.state_focused) + else -> intArrayOf() } - paint.color = mTextColor + canvas.translate(key.x.toFloat(), key.y.toFloat()) + keyBackground.draw(canvas) + if (label?.isNotEmpty() == true) { + // For characters, use large font. For labels like "Done", use small font. + if (label.length > 1) { + paint.textSize = mLabelTextSize.toFloat() + paint.typeface = Typeface.DEFAULT_BOLD + } else { + paint.textSize = mKeyTextSize.toFloat() + paint.typeface = Typeface.DEFAULT + } - canvas.drawText( - label, - (key.width / 2).toFloat(), - key.height / 2 + (paint.textSize - paint.descent()) / 2, - paint - ) + paint.color = mTextColor - if (key.topSmallNumber.isNotEmpty()) { canvas.drawText( - key.topSmallNumber, - key.width - mTopSmallNumberMarginWidth, - mTopSmallNumberMarginHeight, - smallLetterPaint + label, + (key.width / 2).toFloat(), + key.height / 2 + (paint.textSize - paint.descent()) / 2, + paint ) - } - // Turn off drop shadow - paint.setShadowLayer(0f, 0f, 0f, 0) - } else if (key.icon != null && kb != null) { - if (code == KEYCODE_SHIFT) { - key.icon = when (kb.mShiftState) { - SHIFT_OFF -> mShiftOffDrawable - SHIFT_ON_ONE_CHAR -> mShiftOneCharDrawable - else -> mShiftPermDrawable + if (key.topSmallNumber.isNotEmpty()) { + canvas.drawText( + key.topSmallNumber, + key.width - mTopSmallNumberMarginWidth, + mTopSmallNumberMarginHeight, + smallLetterPaint + ) } - } - key.icon?.let { icon -> - if (code == KEYCODE_DELETE || code == KEYCODE_SHIFT || code == KEYCODE_EMOJI || code == KEYCODE_ENTER || - code == KEYCODE_TAB || code == KEYCODE_ARROW_LEFT || code == KEYCODE_ARROW_RIGHT || - code == KEYCODE_ARROW_UP || code == KEYCODE_ARROW_DOWN) { - icon.applyColorFilter(mTextColor) + // Turn off drop shadow + paint.setShadowLayer(0f, 0f, 0f, 0) + } else if (key.icon != null && kb != null) { + if (code == KEYCODE_SHIFT) { + key.icon = when (kb.mShiftState) { + SHIFT_OFF -> mShiftOffDrawable + SHIFT_ON_ONE_CHAR -> mShiftOneCharDrawable + else -> mShiftPermDrawable + } } - val drawableX = (key.width - icon.intrinsicWidth) / 2 - val drawableY = (key.height - icon.intrinsicHeight) / 2 - canvas.translate(drawableX.toFloat(), drawableY.toFloat()) - icon.setBounds(0, 0, icon.intrinsicWidth, icon.intrinsicHeight) - icon.draw(canvas) - canvas.translate(-drawableX.toFloat(), -drawableY.toFloat()) + key.icon?.let { icon -> + if (code == KEYCODE_DELETE || code == KEYCODE_SHIFT || code == KEYCODE_EMOJI || code == KEYCODE_ENTER || + code == KEYCODE_TAB || code == KEYCODE_ARROW_LEFT || code == KEYCODE_ARROW_RIGHT || + code == KEYCODE_ARROW_UP || code == KEYCODE_ARROW_DOWN + ) { + icon.applyColorFilter(mTextColor) + } + + val drawableX = (key.width - icon.intrinsicWidth) / 2 + val drawableY = (key.height - icon.intrinsicHeight) / 2 + canvas.translate(drawableX.toFloat(), drawableY.toFloat()) + icon.setBounds(0, 0, icon.intrinsicWidth, icon.intrinsicHeight) + icon.draw(canvas) + canvas.translate(-drawableX.toFloat(), -drawableY.toFloat()) + } } + canvas.translate(-key.x.toFloat(), -key.y.toFloat()) } - canvas.translate(-key.x.toFloat(), -key.y.toFloat()) - } - // Overlay a dark rectangle to dim the keyboard - if (mMiniKeyboardOnScreen) { - paint.color = Color.BLACK.adjustAlpha(0.3f) - canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint) - } + // Overlay a dark rectangle to dim the keyboard + if (mMiniKeyboardOnScreen) { + paint.color = Color.BLACK.adjustAlpha(0.3f) + canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint) + } - mCanvas!!.restore() + mCanvas!! + } mDrawPending = false mDirtyRect.setEmpty() } @@ -680,7 +719,7 @@ class MainKeyboard @JvmOverloads constructor( mPreviewText!!.setCompoundDrawables(null, null, null, null) try { mPreviewText!!.text = adjustCase(key.label) - } catch (ignored: Exception) { + } catch (_: Exception) { } } @@ -799,11 +838,6 @@ class MainKeyboard @JvmOverloads constructor( private fun openPopupIfRequired(me: MotionEvent): Boolean { return try { - // Check if we have a popup layout specified first. - if (mPopupLayout == 0) { - return false - } - if (mCurrentKey < 0 || mCurrentKey >= mKeys.size) { return false } @@ -819,8 +853,6 @@ class MainKeyboard @JvmOverloads constructor( e.printStackTrace() false } - - } /** @@ -835,11 +867,9 @@ class MainKeyboard @JvmOverloads constructor( if (popupKeyboardId != 0) { mMiniKeyboardContainer = mMiniKeyboardCache[popupKey] if (mMiniKeyboardContainer == null) { - val inflater = - context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - mMiniKeyboardContainer = inflater.inflate(mPopupLayout, null) - mMiniKeyboard = - mMiniKeyboardContainer!!.findViewById(R.id.mini_keyboard_view) as MainKeyboard + val miniKeyboard = createMiniKeyboard() + mMiniKeyboardContainer = miniKeyboard + mMiniKeyboard = miniKeyboard mMiniKeyboard!!.mOnKeyboardActionListener = object : OnKeyboardActionListener { override fun onKey(code: Int) { @@ -887,8 +917,8 @@ class MainKeyboard @JvmOverloads constructor( ) mMiniKeyboardCache[popupKey] = mMiniKeyboardContainer } else { - mMiniKeyboard = - mMiniKeyboardContainer!!.findViewById(R.id.mini_keyboard_view) as MainKeyboard + mMiniKeyboard = mMiniKeyboardContainer as? MainKeyboard + ?: (mMiniKeyboardContainer!!.findViewById(R.id.mini_keyboard_view) as MainKeyboard) } getLocationInWindow(mCoordinates) @@ -1158,14 +1188,14 @@ class MainKeyboard @JvmOverloads constructor( val diff = touchX - mLastSpaceMoveX if (diff < -mSpaceMoveThreshold) { val steps = (-diff) / mSpaceMoveThreshold - for (i in 0 until steps) { + repeat((0 until steps).count()) { mOnKeyboardActionListener?.moveCursorLeft() vibrateIfNeeded() } mLastSpaceMoveX -= steps * mSpaceMoveThreshold } else if (diff > mSpaceMoveThreshold) { val steps = diff / mSpaceMoveThreshold - for (i in 0 until steps) { + repeat((0 until steps).count()) { mOnKeyboardActionListener?.moveCursorRight() vibrateIfNeeded() } @@ -1189,12 +1219,12 @@ class MainKeyboard @JvmOverloads constructor( val diff = mLastX - mLastSpaceMoveX if (diff < -mSpaceMoveThreshold) { - for (i in diff / mSpaceMoveThreshold until 0) { + repeat((diff / mSpaceMoveThreshold until 0).count()) { mOnKeyboardActionListener?.moveCursorLeft() } mLastSpaceMoveX = mLastX } else if (diff > mSpaceMoveThreshold) { - for (i in 0 until diff / mSpaceMoveThreshold) { + repeat((0 until diff / mSpaceMoveThreshold).count()) { mOnKeyboardActionListener?.moveCursorRight() } mLastSpaceMoveX = mLastX diff --git a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboardScreen.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboardScreen.kt index c2eb3e6..0da41d1 100644 --- a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboardScreen.kt +++ b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboardScreen.kt @@ -1,7 +1,8 @@ package com.frogobox.libkeyboard.ui.main +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -16,7 +17,10 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -26,8 +30,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.window.Popup import com.frogobox.libkeyboard.R -import com.frogobox.libkeyboard.ui.theme.FrogoLibKeyboardTheme import com.frogobox.libkeyboard.ui.theme.KeypadActionDark import com.frogobox.libkeyboard.ui.theme.KeypadActionLight import com.frogobox.libkeyboard.ui.theme.KeypadDark @@ -79,51 +83,83 @@ fun MainKeyboardComposable( val defaultKeyColor = if (isDarkTheme) KeypadDark else KeypadLight val actionKeyColor = if (isDarkTheme) KeypadActionDark else KeypadActionLight - Column( - modifier = modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.background) - .padding(horizontal = 4.dp, vertical = 6.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - // Group keys by their approximate row position - val keys = keyboard.mKeys - if (keys.isNotEmpty()) { - val rowYPositions = keys.map { it.y }.distinct().sorted() + var activePopupKey by remember { mutableStateOf(null) } - for (rowY in rowYPositions) { - val rowKeys = keys.filter { it.y == rowY }.sortedBy { it.x } - val totalRowWidth = rowKeys.sumOf { it.width }.coerceAtLeast(1) + Box(modifier = modifier.fillMaxWidth()) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(horizontal = 4.dp, vertical = 6.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + // Group keys by their approximate row position + val keys = keyboard.mKeys + if (keys.isNotEmpty()) { + val rowYPositions = keys.map { it.y }.distinct().sorted() - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(3.dp), - verticalAlignment = Alignment.CenterVertically - ) { - for (key in rowKeys) { - val weight = (key.width.toFloat() / totalRowWidth.toFloat()).coerceAtLeast(0.01f) - val isActionKey = when (key.code) { - ItemMainKeyboard.KEYCODE_SHIFT, - ItemMainKeyboard.KEYCODE_DELETE, - ItemMainKeyboard.KEYCODE_ENTER, - ItemMainKeyboard.KEYCODE_MODE_CHANGE, - ItemMainKeyboard.KEYCODE_TAB -> true - else -> false - } + for (rowY in rowYPositions) { + val rowKeys = keys.filter { it.y == rowY }.sortedBy { it.x } + val totalRowWidth = rowKeys.sumOf { it.width }.coerceAtLeast(1) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(3.dp), + verticalAlignment = Alignment.CenterVertically + ) { + for (key in rowKeys) { + val weight = (key.width.toFloat() / totalRowWidth.toFloat()).coerceAtLeast(0.01f) + val isActionKey = when (key.code) { + ItemMainKeyboard.KEYCODE_SHIFT, + ItemMainKeyboard.KEYCODE_DELETE, + ItemMainKeyboard.KEYCODE_ENTER, + ItemMainKeyboard.KEYCODE_MODE_CHANGE, + ItemMainKeyboard.KEYCODE_TAB -> true + else -> false + } - val keyBackground = if (isActionKey) actionKeyColor else defaultKeyColor + val keyBackground = if (isActionKey) actionKeyColor else defaultKeyColor - KeyItemView( - label = key.label.toString(), - code = key.code, - weight = weight, - backgroundColor = keyBackground, - isActionKey = isActionKey, - onClick = { - onKeyPress(key.code) - onKeyActionUp() + KeyItemView( + label = key.label.toString(), + code = key.code, + backgroundColor = keyBackground, + isActionKey = isActionKey, + onClick = { + onKeyPress(key.code) + onKeyActionUp() + }, + onLongClick = if (key.popupCharacters != null && key.popupCharacters!!.isNotEmpty()) { + { activePopupKey = key } + } else null, + modifier = Modifier.weight(weight) + ) + } + } + } + } + } + + // Render Compose MiniKeyboardPopup overlay on long-press + activePopupKey?.let { popupKey -> + val chars = popupKey.popupCharacters?.map { it.toString() } ?: emptyList() + if (chars.isNotEmpty()) { + Popup( + alignment = Alignment.TopCenter, + onDismissRequest = { activePopupKey = null } + ) { + Box(modifier = Modifier.padding(top = 8.dp)) { + MiniKeyboardPopup( + characters = chars, + onKeySelected = { selectedChar -> + val code = selectedChar.firstOrNull()?.code ?: 0 + if (code != 0) { + onKeyPress(code) + onKeyActionUp() + } + activePopupKey = null }, - modifier = Modifier.weight(weight) + isDarkTheme = isDarkTheme ) } } @@ -132,14 +168,15 @@ fun MainKeyboardComposable( } } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun KeyItemView( label: String, code: Int, - weight: Float, backgroundColor: Color, isActionKey: Boolean, onClick: () -> Unit, + onLongClick: (() -> Unit)? = null, modifier: Modifier = Modifier ) { val interactionSource = remember { MutableInteractionSource() } @@ -148,10 +185,11 @@ private fun KeyItemView( modifier = modifier .height(46.dp) .clip(RoundedCornerShape(6.dp)) - .clickable( + .combinedClickable( interactionSource = interactionSource, indication = ripple(bounded = true), - onClick = onClick + onClick = onClick, + onLongClick = onLongClick ), shape = RoundedCornerShape(6.dp), color = backgroundColor, diff --git a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MiniKeyboardPopup.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MiniKeyboardPopup.kt new file mode 100644 index 0000000..5d566c0 --- /dev/null +++ b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MiniKeyboardPopup.kt @@ -0,0 +1,133 @@ +package com.frogobox.libkeyboard.ui.main + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.frogobox.libkeyboard.ui.theme.FrogoLibKeyboardTheme + +/** + * Modern Jetpack Compose Mini Keyboard Popup component replacing legacy keyboard_main_mini.xml. + * Renders a horizontal capsule strip containing alternate characters on key long-press. + */ +@Composable +fun MiniKeyboardPopup( + characters: List, + onKeySelected: (String) -> Unit, + modifier: Modifier = Modifier, + selectedIndex: Int = -1, + isDarkTheme: Boolean = false +) { + val containerColor = if (isDarkTheme) Color(0xFF2B2B2C) else Color(0xFFECEFF1) + val selectedItemColor = MaterialTheme.colorScheme.primaryContainer + val defaultItemColor = Color.Transparent + val defaultTextColor = if (isDarkTheme) Color.White else Color(0xFF1E1E1E) + val selectedTextColor = MaterialTheme.colorScheme.onPrimaryContainer + + Surface( + modifier = modifier + .wrapContentWidth() + .height(52.dp), + shape = RoundedCornerShape(12.dp), + color = containerColor, + shadowElevation = 8.dp, + tonalElevation = 4.dp + ) { + Row( + modifier = Modifier + .wrapContentWidth() + .padding(horizontal = 4.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalAlignment = Alignment.CenterVertically + ) { + characters.forEachIndexed { index, char -> + val isSelected = index == selectedIndex + val backgroundColor by animateColorAsState( + targetValue = if (isSelected) selectedItemColor else defaultItemColor, + animationSpec = tween(durationMillis = 120), + label = "MiniKeyHighlight" + ) + val textColor = if (isSelected) selectedTextColor else defaultTextColor + val interactionSource = remember { MutableInteractionSource() } + + Box( + modifier = Modifier + .defaultMinSize(minWidth = 38.dp) + .height(44.dp) + .clip(RoundedCornerShape(8.dp)) + .background(backgroundColor) + .clickable( + interactionSource = interactionSource, + indication = ripple(bounded = true), + onClick = { onKeySelected(char) } + ), + contentAlignment = Alignment.Center + ) { + Text( + text = char, + fontSize = 18.sp, + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium, + color = textColor, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 6.dp) + ) + } + } + } + } +} + +@Preview(name = "Mini Keyboard Light", showBackground = true) +@Composable +private fun MiniKeyboardPopupLightPreview() { + FrogoLibKeyboardTheme(darkTheme = false) { + Box(modifier = Modifier.padding(16.dp)) { + MiniKeyboardPopup( + characters = listOf("á", "à", "â", "ä", "ã", "å"), + selectedIndex = 2, + onKeySelected = {}, + isDarkTheme = false + ) + } + } +} + +@Preview(name = "Mini Keyboard Dark", showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun MiniKeyboardPopupDarkPreview() { + FrogoLibKeyboardTheme(darkTheme = true) { + Box(modifier = Modifier.padding(16.dp)) { + MiniKeyboardPopup( + characters = listOf("1", "!", "@", "#", "$"), + selectedIndex = 0, + onKeySelected = {}, + isDarkTheme = true + ) + } + } +} diff --git a/lib-keyboard/src/main/res/drawable/ic_keyboard_arrow_down.xml b/lib-keyboard/src/main/res/drawable/ic_keyboard_arrow_down.xml deleted file mode 100644 index 50613c2..0000000 --- a/lib-keyboard/src/main/res/drawable/ic_keyboard_arrow_down.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/lib-keyboard/src/main/res/drawable/ic_keyboard_arrow_up.xml b/lib-keyboard/src/main/res/drawable/ic_keyboard_arrow_up.xml deleted file mode 100644 index bd1add9..0000000 --- a/lib-keyboard/src/main/res/drawable/ic_keyboard_arrow_up.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/lib-keyboard/src/main/res/drawable/keypad_emoji.xml b/lib-keyboard/src/main/res/drawable/keypad_emoji.xml deleted file mode 100644 index 4264e74..0000000 --- a/lib-keyboard/src/main/res/drawable/keypad_emoji.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/lib-keyboard/src/main/res/layout/item_keyboard_emoji.xml b/lib-keyboard/src/main/res/layout/item_keyboard_emoji.xml deleted file mode 100644 index 1803ba8..0000000 --- a/lib-keyboard/src/main/res/layout/item_keyboard_emoji.xml +++ /dev/null @@ -1,11 +0,0 @@ - - diff --git a/lib-keyboard/src/main/res/layout/item_keyboard_main.xml b/lib-keyboard/src/main/res/layout/item_keyboard_main.xml deleted file mode 100644 index 771b582..0000000 --- a/lib-keyboard/src/main/res/layout/item_keyboard_main.xml +++ /dev/null @@ -1,10 +0,0 @@ - diff --git a/lib-keyboard/src/main/res/layout/keyboard_emoji.xml b/lib-keyboard/src/main/res/layout/keyboard_emoji.xml deleted file mode 100644 index 7d6ddae..0000000 --- a/lib-keyboard/src/main/res/layout/keyboard_emoji.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/lib-keyboard/src/main/res/layout/keyboard_main_mini.xml b/lib-keyboard/src/main/res/layout/keyboard_main_mini.xml deleted file mode 100644 index 001639b..0000000 --- a/lib-keyboard/src/main/res/layout/keyboard_main_mini.xml +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/lib-keyboard/src/main/res/values-night/colors.xml b/lib-keyboard/src/main/res/values-night/colors.xml index 3b30b0c..4c8fa4b 100644 --- a/lib-keyboard/src/main/res/values-night/colors.xml +++ b/lib-keyboard/src/main/res/values-night/colors.xml @@ -2,8 +2,6 @@ #FFFFFF - #000000 - #FFFFFF diff --git a/lib-keyboard/src/main/res/values/colors.xml b/lib-keyboard/src/main/res/values/colors.xml index 4a677b4..be36eb1 100644 --- a/lib-keyboard/src/main/res/values/colors.xml +++ b/lib-keyboard/src/main/res/values/colors.xml @@ -11,15 +11,8 @@ #FF000000 #FFFFFFFF - #424242 - - #C53741 - #41AF60 - #FFF57C00 #000000 - #FFFFFF - #000000 #FCFCFF diff --git a/lib-keyboard/src/main/res/values/dimens.xml b/lib-keyboard/src/main/res/values/dimens.xml index 653d829..218ca6c 100644 --- a/lib-keyboard/src/main/res/values/dimens.xml +++ b/lib-keyboard/src/main/res/values/dimens.xml @@ -11,18 +11,9 @@ 60dp -10dp - - 46dp - 32dp - 46dp - 42dp - 56dp - 12dp - 22sp 26sp 16sp - 28sp diff --git a/lib-keyboard/src/main/res/values/ids.xml b/lib-keyboard/src/main/res/values/ids.xml new file mode 100644 index 0000000..c9097dc --- /dev/null +++ b/lib-keyboard/src/main/res/values/ids.xml @@ -0,0 +1,4 @@ + + + + diff --git a/lib-keyboard/src/main/res/values/strings.xml b/lib-keyboard/src/main/res/values/strings.xml index a22c765..c916a70 100644 --- a/lib-keyboard/src/main/res/values/strings.xml +++ b/lib-keyboard/src/main/res/values/strings.xml @@ -1,6 +1,4 @@ - Change Keyboard - Delete Change keyboard type Shift @@ -9,10 +7,6 @@ Back Emojis - Welcome to Frogo Keyboard - For using this Frogo Keyboard you need to go to your settings and enable the Frogo Keyboard. After that, when you want to use this Frogo Keyboard, you need to set it for default keyboard. - After that, go to any chat application and share content :) - Bengali Bulgarian English diff --git a/lib-keyboard/src/main/res/xml/keys_letters_english_qwerty.xml b/lib-keyboard/src/main/res/xml/keys_letters_english_qwerty.xml deleted file mode 100644 index 9fc0225..0000000 --- a/lib-keyboard/src/main/res/xml/keys_letters_english_qwerty.xml +++ /dev/null @@ -1,178 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib-keyboard/src/test/java/com/frogobox/libkeyboard/KeyboardPopupsComposeTest.kt b/lib-keyboard/src/test/java/com/frogobox/libkeyboard/KeyboardPopupsComposeTest.kt new file mode 100644 index 0000000..8d0902d --- /dev/null +++ b/lib-keyboard/src/test/java/com/frogobox/libkeyboard/KeyboardPopupsComposeTest.kt @@ -0,0 +1,77 @@ +package com.frogobox.libkeyboard + +import android.content.Context +import android.view.View +import androidx.test.core.app.ApplicationProvider +import com.frogobox.libkeyboard.ui.main.MainKeyboard +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Unit & Robolectric test suite for TASK-024: + * Verifies that MainKeyboard initializes key preview and mini keyboard programmatically + * without relying on legacy XML layouts (item_keyboard_main.xml & keyboard_main_mini.xml). + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34]) +class KeyboardPopupsComposeTest { + + private lateinit var context: Context + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + } + + @Test + fun testMainKeyboardInit_withoutXmlLayouts_initializesSuccessfully() { + // Given programmatic instantiation without item_keyboard_main.xml or keyboard_main_mini.xml + val keyboard = MainKeyboard(context, null) + + // Then view must initialize properly + assertNotNull("Keyboard instance must not be null", keyboard) + assertTrue("Key text size must be greater than zero", keyboard.getKeyTextSize() > 0) + assertNotNull("Background must be non-null", keyboard.background) + } + + @Test + fun testSetKeyTextSize_updatesPropertyCorrectly() { + val keyboard = MainKeyboard(context, null) + val newSize = 36 + + keyboard.setKeyTextSize(newSize) + assertEquals("getKeyTextSize must return the updated size", newSize, keyboard.getKeyTextSize()) + } + + @Test + fun testMiniKeyboard_backwardCompatibilityId_remainsDefined() { + // Verify R.id.mini_keyboard_view remains accessible and defined via ids.xml + val resId = R.id.mini_keyboard_view + assertTrue("mini_keyboard_view resource ID must be defined and non-zero", resId != 0) + + // Verify simulated mini_keyboard_view behaves properly in visibility check + val keyboard = MainKeyboard(context, null) + val miniView = View(context).apply { id = R.id.mini_keyboard_view } + + keyboard.onVisibilityChanged(miniView, View.VISIBLE) + assertNotNull("Keyboard background must remain valid after visibility change", keyboard.background) + } + + @Test + fun testMiniKeyboardCharacters_parsingAndSelectionLogic() { + val testChars = listOf("á", "à", "â", "ä", "ã", "å") + var selectedChar: String? = null + + val onSelect: (String) -> Unit = { selectedChar = it } + onSelect(testChars[2]) + + assertEquals("Selected character should match callback input", "â", selectedChar) + assertEquals("Character count matches input list", 6, testChars.size) + } +} From 07a46dbb86e2c1c087ab771aba35a67fd4d8f5bf Mon Sep 17 00:00:00 2001 From: amirisback Date: Sun, 20 Sep 2026 15:34:10 +0700 Subject: [PATCH 7/8] feat: Jetpack compose --- .../appkeyboard/ui/test/TestActivity.kt | 4 - .../appkeyboard/ui/test/TestScreen.kt | 630 +----------------- 2 files changed, 2 insertions(+), 632 deletions(-) diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestActivity.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestActivity.kt index 30a39f6..03f0348 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestActivity.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestActivity.kt @@ -82,7 +82,6 @@ class TestActivity : BaseComposeActivity() { val metrics by viewModel.metrics.collectAsState() val activeTab by viewModel.activeTab.collectAsState() val autoTextList by viewModel.autoTextList.collectAsState() - val remoteApiUiState by viewModel.remoteApiUiState.collectAsState() TestScreen( sandboxText = sandboxText, @@ -90,15 +89,12 @@ class TestActivity : BaseComposeActivity() { activeTab = activeTab, autoTextList = autoTextList, isKeyboardActive = isUsingFrogoKeyboard, - remoteApiUiState = remoteApiUiState, dummyOptions = dummyOptions, onTextChanged = viewModel::onTextChanged, onInsertText = viewModel::onInsertText, onClearText = viewModel::onClearText, onResetTimer = viewModel::onResetTimer, onTabSelected = viewModel::onTabSelected, - onFetchRemoteData = viewModel::fetchRemoteData, - onResetRemoteData = viewModel::resetRemoteData, onChangeKeyboard = { (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager).showInputMethodPicker() }, diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestScreen.kt index 8d91be6..8226237 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestScreen.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestScreen.kt @@ -28,15 +28,10 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Notes import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Clear -import androidx.compose.material.icons.filled.CloudDownload -import androidx.compose.material.icons.filled.CloudOff -import androidx.compose.material.icons.filled.CloudQueue import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.Inbox import androidx.compose.material.icons.filled.Keyboard -import androidx.compose.material.icons.filled.PlayCircle import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings @@ -48,7 +43,6 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api @@ -74,17 +68,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.frogobox.appkeyboard.data.remote.model.DataItemResponse import com.frogobox.appkeyboard.model.AutoTextEntity import com.frogobox.appkeyboard.ui.theme.compose.FrogoKeyboardTheme import com.frogobox.appkeyboard.ui.theme.compose.FrogoPrimary @@ -104,15 +95,12 @@ fun TestScreen( activeTab: Int, autoTextList: List, isKeyboardActive: Boolean, - remoteApiUiState: DataApiUiState = DataApiUiState.Idle, dummyOptions: List = emptyList(), onTextChanged: (String) -> Unit, onInsertText: (String) -> Unit, onClearText: () -> Unit, onResetTimer: () -> Unit, onTabSelected: (Int) -> Unit, - onFetchRemoteData: () -> Unit = {}, - onResetRemoteData: () -> Unit = {}, onChangeKeyboard: () -> Unit, onGoToSettings: () -> Unit, onBackClick: (() -> Unit)? = null, @@ -166,9 +154,9 @@ fun TestScreen( // 2. Real-time Live Typing Metrics Card LiveMetricsCard(metrics = metrics) - // 3. Tab Navigation Row (4 Tabs) + // 3. Tab Navigation Row (3 Tabs) PrimaryTabRow( - selectedTabIndex = activeTab, + selectedTabIndex = activeTab.coerceIn(0, 2), containerColor = MaterialTheme.colorScheme.surface, contentColor = FrogoPrimary ) { @@ -190,12 +178,6 @@ fun TestScreen( text = { Text("AutoText", fontWeight = FontWeight.SemiBold) }, icon = { Icon(Icons.AutoMirrored.Filled.Notes, contentDescription = null, modifier = Modifier.size(20.dp)) } ) - Tab( - selected = activeTab == 3, - onClick = { onTabSelected(3) }, - text = { Text("Remote API", fontWeight = FontWeight.SemiBold) }, - icon = { Icon(Icons.Default.CloudDownload, contentDescription = null, modifier = Modifier.size(20.dp)) } - ) } // 4. Tab Body Content @@ -239,16 +221,6 @@ fun TestScreen( onTabSelected(0) } ) - 3 -> RemoteApiTabContent( - uiState = remoteApiUiState, - onFetchClicked = onFetchRemoteData, - onResetClicked = onResetRemoteData, - onInsertToSandbox = { text -> - onInsertText(text) - Toast.makeText(context, "Inserted remote data to sandbox", Toast.LENGTH_SHORT).show() - onTabSelected(0) - } - ) } Spacer(modifier = Modifier.height(16.dp)) @@ -785,601 +757,3 @@ private fun AutoTextTabContent( } } } - -/** - * 4. Remote API Tab Content Component - */ -@Composable -private fun RemoteApiTabContent( - uiState: DataApiUiState, - onFetchClicked: () -> Unit, - onResetClicked: () -> Unit, - onInsertToSandbox: (String) -> Unit, - modifier: Modifier = Modifier -) { - Column( - modifier = modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(14.dp) - ) { - // 1. Endpoint & Status Header Banner - ApiHeaderBanner( - uiState = uiState, - onFetchClicked = onFetchClicked, - onResetClicked = onResetClicked - ) - - // 2. State-driven Body - when (uiState) { - is DataApiUiState.Idle -> ApiIdleView(onFetchClicked = onFetchClicked) - is DataApiUiState.Loading -> ApiLoadingView() - is DataApiUiState.Error -> ApiErrorView( - errorMessage = uiState.message, - onRetryClicked = onFetchClicked - ) - is DataApiUiState.Success -> { - if (uiState.items.isEmpty()) { - ApiEmptyView(onRefreshClicked = onFetchClicked) - } else { - ApiSuccessListView( - items = uiState.items, - onInsertToSandbox = onInsertToSandbox - ) - } - } - } - } -} - -@Composable -private fun ApiHeaderBanner( - uiState: DataApiUiState, - onFetchClicked: () -> Unit, - onResetClicked: () -> Unit, - modifier: Modifier = Modifier -) { - Card( - modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.35f)), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(14.dp), - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "🌐 Remote Data Ingestion", - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface - ) - Box( - modifier = Modifier - .clip(RoundedCornerShape(6.dp)) - .background(FrogoStatusSuccess.copy(alpha = 0.12f)) - .padding(horizontal = 8.dp, vertical = 3.dp) - ) { - Text( - text = "● Online: Port 3000", - fontSize = 11.sp, - fontWeight = FontWeight.SemiBold, - color = FrogoStatusSuccess - ) - } - } - - // Monospace endpoint badge - Box( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant) - .padding(horizontal = 10.dp, vertical = 6.dp) - ) { - Text( - text = "🔗 http://192.168.100.6:3000/api/data.json", - style = MaterialTheme.typography.bodySmall, - fontFamily = FontFamily.Monospace, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - // Metadata summary when Success - if (uiState is DataApiUiState.Success) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Box( - modifier = Modifier - .clip(RoundedCornerShape(6.dp)) - .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)) - .padding(horizontal = 8.dp, vertical = 3.dp) - ) { - Text( - text = "🏷️ ${uiState.total} Items Available", - style = MaterialTheme.typography.labelSmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary - ) - } - - Text( - text = "🕒 ${uiState.lastUpdatedWib}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - - // Primary action row - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Button( - onClick = onFetchClicked, - modifier = Modifier - .weight(1f) - .height(48.dp), - shape = RoundedCornerShape(12.dp), - colors = ButtonDefaults.buttonColors(containerColor = FrogoPrimary) - ) { - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = "Fetch Live Data", - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(6.dp)) - Text( - text = if (uiState is DataApiUiState.Success) "Refresh Data" else "Fetch Live Data", - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.SemiBold - ) - } - - if (uiState !is DataApiUiState.Idle) { - OutlinedButton( - onClick = onResetClicked, - modifier = Modifier.height(48.dp), - shape = RoundedCornerShape(12.dp) - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = "Reset State", - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = "Reset", - style = MaterialTheme.typography.labelLarge - ) - } - } - } - } - } -} - -@Composable -private fun ApiIdleView( - onFetchClicked: () -> Unit, - modifier: Modifier = Modifier -) { - Card( - modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.25f)) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Box( - modifier = Modifier - .size(56.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Default.CloudQueue, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(32.dp) - ) - } - - Text( - text = "No Data Fetched Yet", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface - ) - - Text( - text = "Tap 'Fetch Live Data' to load dynamic content from the local server (http://192.168.100.6:3000/api/data.json).", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - - Button( - onClick = onFetchClicked, - modifier = Modifier - .fillMaxWidth() - .height(48.dp), - shape = RoundedCornerShape(12.dp), - colors = ButtonDefaults.buttonColors(containerColor = FrogoPrimary) - ) { - Icon( - imageVector = Icons.Default.CloudDownload, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text("Fetch Data Now", fontWeight = FontWeight.SemiBold) - } - } - } -} - -@Composable -private fun ApiLoadingView(modifier: Modifier = Modifier) { - Card( - modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.25f)) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(14.dp) - ) { - CircularProgressIndicator( - modifier = Modifier.size(36.dp), - strokeWidth = 3.dp, - color = FrogoPrimary - ) - Text( - text = "Connecting to server & fetching data...", - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = "Executing on Dispatchers.IO via Kotlin Coroutines & Flow", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } -} - -@Composable -private fun ApiErrorView( - errorMessage: String, - onRetryClicked: () -> Unit, - modifier: Modifier = Modifier -) { - Card( - modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.25f) - ), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.error.copy(alpha = 0.4f)) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp) - ) { - Box( - modifier = Modifier - .size(36.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.error.copy(alpha = 0.15f)), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Default.CloudOff, - contentDescription = "Error Icon", - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(20.dp) - ) - } - - Text( - text = "Failed to Fetch Remote Data", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.error - ) - } - - Text( - text = errorMessage, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface - ) - - Box( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colorScheme.surface) - .padding(10.dp) - ) { - Text( - text = "Troubleshooting:\n1. Verify Node.js server is running on port 3000 at 192.168.100.6.\n2. Ensure device and server are on the same local Wi-Fi subnet.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - Button( - onClick = onRetryClicked, - modifier = Modifier - .fillMaxWidth() - .height(48.dp), - shape = RoundedCornerShape(12.dp), - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error) - ) { - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text("Retry Connection", fontWeight = FontWeight.SemiBold) - } - } - } -} - -@Composable -private fun ApiEmptyView( - onRefreshClicked: () -> Unit, - modifier: Modifier = Modifier -) { - Card( - modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.25f)) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Icon( - imageVector = Icons.Default.Inbox, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(40.dp) - ) - Text( - text = "Server returned 0 records.", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - Button( - onClick = onRefreshClicked, - modifier = Modifier.height(48.dp), - shape = RoundedCornerShape(12.dp) - ) { - Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(18.dp)) - Spacer(modifier = Modifier.width(6.dp)) - Text("Refresh") - } - } - } -} - -@Composable -private fun ApiSuccessListView( - items: List, - onInsertToSandbox: (String) -> Unit, - modifier: Modifier = Modifier -) { - Column( - modifier = modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - items.forEach { item -> - ApiDataItemCard( - item = item, - onInsertToSandbox = onInsertToSandbox - ) - } - } -} - -@Composable -private fun ApiDataItemCard( - item: DataItemResponse, - onInsertToSandbox: (String) -> Unit, - modifier: Modifier = Modifier -) { - val context = LocalContext.current - - Card( - modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(14.dp), - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - // Badges row - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { - Text( - text = "#ROW ${item.displayIndex}", - style = MaterialTheme.typography.labelSmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier - .background( - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f), - shape = RoundedCornerShape(6.dp) - ) - .padding(horizontal = 6.dp, vertical = 2.dp) - ) - if (item.isVideo == true) { - Text( - text = "🎬 Video", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .background( - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(6.dp) - ) - .padding(horizontal = 6.dp, vertical = 2.dp) - ) - } - } - Text( - text = item.fileSize ?: "-", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - // Media & Caption row - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.Top - ) { - // Media Icon / Thumbnail Box - Box( - modifier = Modifier - .size(56.dp) - .clip(RoundedCornerShape(10.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Default.PlayCircle, - contentDescription = "Media Preview", - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(28.dp) - ) - } - - // Text Details Column - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(3.dp) - ) { - Text( - text = item.displayTitle, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - item.displaySubtitle?.let { subtitle -> - Text( - text = "👤 $subtitle", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - item.displayTimestamp?.let { timestamp -> - Text( - text = "🕒 $timestamp", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f) - ) - } - } - } - - // Action Buttons Row - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Button( - onClick = { - val textToInsert = item.displayBody ?: item.displayTitle - onInsertToSandbox(textToInsert) - }, - modifier = Modifier - .weight(1f) - .height(48.dp), - shape = RoundedCornerShape(10.dp), - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary) - ) { - Icon( - imageVector = Icons.Default.Edit, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - Spacer(modifier = Modifier.width(6.dp)) - Text("Insert Sandbox", style = MaterialTheme.typography.labelMedium) - } - - if (!item.driveLink.isNullOrEmpty()) { - OutlinedButton( - onClick = { - val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - clipboard.setPrimaryClip(ClipData.newPlainText("Drive Link", item.driveLink)) - Toast.makeText(context, "Drive link copied!", Toast.LENGTH_SHORT).show() - }, - modifier = Modifier.height(48.dp), - shape = RoundedCornerShape(10.dp) - ) { - Icon( - imageVector = Icons.Default.ContentCopy, - contentDescription = "Copy Drive Link", - modifier = Modifier.size(16.dp) - ) - } - } - } - } - } -} From 79593cd72b25690ff5a2441340662b829cbcbac4 Mon Sep 17 00:00:00 2001 From: amirisback Date: Sun, 20 Sep 2026 17:07:15 +0700 Subject: [PATCH 8/8] feat: add Jetpack Compose UI screens, components, and themes for keyboard app --- DESIGN.md | 82 +++++++++ .../ui/autotext/AutoTextDetailScreen.kt | 100 +++++------ .../ui/autotext/AutoTextEditorScreen.kt | 101 ++++++------ .../appkeyboard/ui/autotext/AutoTextScreen.kt | 97 ++++++----- .../ui/keyboard/form/FormKeyboardScreen.kt | 15 +- .../ui/keyboard/movie/MovieKeyboardScreen.kt | 8 +- .../ui/keyboard/news/NewsKeyboardScreen.kt | 8 +- .../ui/keyboard/root/KeyboardFeatureHeader.kt | 155 ++++++++++++++---- .../ui/keyboard/root/KeyboardSuggestionBar.kt | 13 +- .../TemplateTextKeyboardScreen.kt | 12 +- .../ui/language/KeyboardLanguageScreen.kt | 63 ++++--- .../appkeyboard/ui/main/MainScreen.kt | 126 +++++++------- .../appkeyboard/ui/sound/SoundScreen.kt | 91 +++++----- .../appkeyboard/ui/test/TestScreen.kt | 91 +++++----- .../appkeyboard/ui/theme/ThemeScreen.kt | 59 +++---- .../appkeyboard/ui/theme/compose/Color.kt | 65 +++++--- .../ui/theme/compose/FrogoEmptyView.kt | 50 ++++-- .../appkeyboard/ui/theme/compose/Theme.kt | 40 +++-- .../appkeyboard/ui/toggle/ToggleScreen.kt | 30 ++-- .../appkeyboard/KeyboardImeComposeUnitTest.kt | 21 +++ .../ui/emoji/EmojiKeyboardScreen.kt | 10 +- .../libkeyboard/ui/main/MainKeyboardScreen.kt | 2 + .../ui/theme/FrogoLibKeyboardTheme.kt | 50 +++--- 23 files changed, 795 insertions(+), 494 deletions(-) create mode 100644 DESIGN.md diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..ee1f102 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,82 @@ +# DESIGN.md — Frogo Keyboard Visual Direction & Design System + +> Anti-Slop Mode: DURING +> Dial: ENERGY 2 / RHYTHM 2 / MOTION 2 +> Reading this as: Native Android Keyboard & Tooling App for everyday typing and productivity users, in a Tactile & Modern Tooling Material 3 style. + +--- + +## 1. Brand Identity & Character + +Frogo Keyboard is a precision mobile input method and utility suite. Rather than appearing as a generic AI template application or a whimsical toy, the design embodies **Tactile & Modern Tooling**: +- **Tone:** Professional, reliable, tactile, and clear. +- **Surface Feel:** Solid, grounded surfaces with clean 1dp borders instead of muddy floating blur or excessive drop shadows. +- **Keycap Feel:** High-contrast keycaps with distinct touch feedback, crisp typographic legends, and ergonomic spacing. +- **Copywriting:** Direct, human, informative text. Strict ban on em dashes (`—`) and generic AI marketing buzzwords. + +--- + +## 2. Core Color Palette (WCAG AA Compliant) + +Capped at 2 core colors + 1 accent color (R-29), with neutral ground bases: + +### Light Theme +- **Primary (Mechanical Steel Blue):** `#2563EB` (Contrast ratio >= 4.5:1 on white/surface) +- **Primary Container:** `#EFF6FF` (Subtle tint for selected state) +- **Background:** `#F8FAFC` (Clean slate-white ground plane) +- **Surface:** `#FFFFFF` (Card surfaces) +- **Border / Outline:** `#E2E8F0` (Tactile 1dp border) +- **On Surface / Text Primary:** `#0F172A` (Slate 900, 14.2:1 contrast ratio) +- **On Surface Variant / Text Secondary:** `#475569` (Slate 600, 5.8:1 contrast ratio) + +### Dark Theme +- **Primary (Precision Steel Blue Dark):** `#3B82F6` (Contrast ratio >= 4.5:1 on dark surfaces) +- **Primary Container:** `#1E3A8A` (Deep navy container) +- **Background:** `#0F172A` (Deep slate ground plane) +- **Surface:** `#1E293B` (Elevated slate surface) +- **Border / Outline:** `#334155` (Crisp dark tactile border) +- **On Surface / Text Primary:** `#F8FAFC` (Crisp white-slate) +- **On Surface Variant / Text Secondary:** `#94A3B8` (Legible muted slate) + +### Semantic Status Colors +- **Success (Active / Enabled):** `#15803D` (Light) / `#22C55E` (Dark) +- **Warning (Attention / Action Needed):** `#B45309` (Light) / `#F59E0B` (Dark) +- **Error (Failed / Inactive):** `#B91C1C` (Light) / `#EF4444` (Dark) + +--- + +## 3. Shape, Radii, & Elevation Hierarchy + +No uniform pill shapes everywhere (R-11). Distinct functional radii: +- **Card Surfaces:** `12.dp` (Clean, grounded corners) +- **Chips, Keycaps, & Buttons:** `8.dp` (Ergonomic tap target) +- **Dialogs & Bottom Sheets:** `16.dp` (Modal focus) +- **Status Indicators / Avatars:** `CircleShape` (Only for circular glyph badges) +- **Elevation:** Flat `0.dp` or subtle `1.dp` accompanied by a `1.dp` crisp border (`BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)`). No generic floating clouds (R-12). + +--- + +## 4. Typography Scale + +- **Title Large:** 22sp, Bold, 28sp line height (Screen headers) +- **Title Medium:** 18sp, SemiBold, 24sp line height (Section titles) +- **Title Small:** 15sp, SemiBold, 20sp line height (Card titles) +- **Body Large:** 16sp, Normal, 24sp line height (Primary descriptions) +- **Body Medium:** 14sp, Normal, 20sp line height (Secondary descriptions, form labels) +- **Body Small:** 12sp, Normal, 16sp line height (Metadata, hints) +- **Label Large:** 14sp, Medium, 20sp line height (Button text, action labels) +- **Label Small:** 11sp, SemiBold, 14sp line height (Badges, indicators) + +--- + +## 5. Anti-Slop Enforcement Rules + +1. **R-01 (Color):** Zero generic purple-to-pink gradients or neon glow. All colors follow the palette above. +2. **R-02 (Copywriting):** Absolutely NO em dash (`—`) in any UI text or code comments. Use comma, period, colon, or parentheses. +3. **R-03 (Mobile Responsiveness):** Minimum 48dp tap target for interactive elements. Zero horizontal scroll overflow on standard screens. +4. **R-06 (Typography):** No uppercase tracking slop (e.g. replace `NO DATA FOUND` with clear descriptive copy). +5. **R-10 & R-13 (Glass & Glow):** No blur overlays or decorative glows. Clean, matte, tactile surfaces. +6. **R-16 (Buzzwords):** No words like "AI Powered", "Seamless", "Revolutionary", "Next-Gen". Use direct descriptive language. +7. **R-25 (Contrast):** Every text and icon pair satisfies WCAG AA (>= 4.5:1 for normal text). +8. **R-27 (UI States):** Empty, Loading, and Content states present and properly styled on every screen. +9. **R-34 (Theme Integrity):** Light and Dark themes both fully functional and tested. diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextDetailScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextDetailScreen.kt index 7b47779..4999299 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextDetailScreen.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextDetailScreen.kt @@ -106,13 +106,14 @@ fun AutoTextDetailScreen( bottomBar = { Surface( color = MaterialTheme.colorScheme.surface, - tonalElevation = 3.dp, - shadowElevation = 8.dp + tonalElevation = 0.dp, + shadowElevation = 0.dp, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) ) { Row( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .padding(horizontal = 16.dp, vertical = 12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp) ) { // Delete Button @@ -120,23 +121,23 @@ fun AutoTextDetailScreen( onClick = { showDeleteDialog = true }, modifier = Modifier .weight(1f) - .height(52.dp), - shape = RoundedCornerShape(16.dp), + .height(48.dp), + shape = RoundedCornerShape(8.dp), colors = ButtonDefaults.outlinedButtonColors( contentColor = FrogoStatusFailed ), - border = BorderStroke(1.dp, FrogoStatusFailed.copy(alpha = 0.4f)) + border = BorderStroke(1.dp, FrogoStatusFailed.copy(alpha = 0.6f)) ) { Icon( imageVector = Icons.Default.Delete, - contentDescription = "Delete", + contentDescription = "Hapus", modifier = Modifier.size(18.dp) ) Spacer(modifier = Modifier.width(6.dp)) Text( - text = "Delete", + text = "Hapus", style = MaterialTheme.typography.labelLarge.copy( - fontSize = 15.sp, + fontSize = 14.sp, fontWeight = FontWeight.SemiBold ), color = FrogoStatusFailed @@ -148,8 +149,8 @@ fun AutoTextDetailScreen( onClick = onEditClick, modifier = Modifier .weight(1f) - .height(52.dp), - shape = RoundedCornerShape(16.dp), + .height(48.dp), + shape = RoundedCornerShape(8.dp), colors = ButtonDefaults.buttonColors( containerColor = FrogoPrimary ) @@ -162,9 +163,9 @@ fun AutoTextDetailScreen( ) Spacer(modifier = Modifier.width(6.dp)) Text( - text = "Edit", + text = "Ubah", style = MaterialTheme.typography.labelLarge.copy( - fontSize = 15.sp, + fontSize = 14.sp, fontWeight = FontWeight.SemiBold ), color = Color.White @@ -186,17 +187,17 @@ fun AutoTextDetailScreen( // Header Hero Card Card( modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(20.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), + shape = RoundedCornerShape(12.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface ), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) ) { Column( modifier = Modifier .fillMaxWidth() - .padding(20.dp) + .padding(16.dp) ) { Row( modifier = Modifier.fillMaxWidth(), @@ -205,8 +206,8 @@ fun AutoTextDetailScreen( ) { Box( modifier = Modifier - .size(44.dp) - .clip(RoundedCornerShape(12.dp)) + .size(40.dp) + .clip(RoundedCornerShape(8.dp)) .background(FrogoPrimary.copy(alpha = 0.12f)), contentAlignment = Alignment.Center ) { @@ -214,26 +215,26 @@ fun AutoTextDetailScreen( imageVector = Icons.AutoMirrored.Filled.ShortText, contentDescription = null, tint = FrogoPrimary, - modifier = Modifier.size(24.dp) + modifier = Modifier.size(22.dp) ) } IconButton(onClick = onShareSnippet) { Icon( imageVector = Icons.Default.Share, - contentDescription = "Share", + contentDescription = "Bagikan", tint = MaterialTheme.colorScheme.onSurfaceVariant ) } } - Spacer(modifier = Modifier.height(14.dp)) + Spacer(modifier = Modifier.height(12.dp)) Text( text = data.title, style = MaterialTheme.typography.titleLarge.copy( fontWeight = FontWeight.Bold, - fontSize = 22.sp + fontSize = 20.sp ), color = MaterialTheme.colorScheme.onSurface ) @@ -246,11 +247,12 @@ fun AutoTextDetailScreen( verticalAlignment = Alignment.CenterVertically ) { Surface( - shape = RoundedCornerShape(8.dp), - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f) + shape = RoundedCornerShape(6.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f), + border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) ) { Text( - text = "${data.body.length} characters", + text = "${data.body.length} karakter", style = MaterialTheme.typography.labelSmall.copy( fontWeight = FontWeight.Medium, fontSize = 11.sp @@ -261,11 +263,12 @@ fun AutoTextDetailScreen( } Surface( - shape = RoundedCornerShape(8.dp), - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f) + shape = RoundedCornerShape(6.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f), + border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) ) { Text( - text = "$wordCount words", + text = "$wordCount kata", style = MaterialTheme.typography.labelSmall.copy( fontWeight = FontWeight.Medium, fontSize = 11.sp @@ -276,15 +279,15 @@ fun AutoTextDetailScreen( } } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(14.dp)) // Primary Copy Button Button( onClick = onCopySnippet, modifier = Modifier .fillMaxWidth() - .height(46.dp), - shape = RoundedCornerShape(12.dp), + .height(44.dp), + shape = RoundedCornerShape(8.dp), colors = ButtonDefaults.buttonColors( containerColor = FrogoPrimary.copy(alpha = 0.12f), contentColor = FrogoPrimary @@ -293,12 +296,12 @@ fun AutoTextDetailScreen( ) { Icon( imageVector = Icons.Default.ContentCopy, - contentDescription = "Copy text", + contentDescription = "Salin teks", modifier = Modifier.size(18.dp) ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Copy to Clipboard", + text = "Salin ke Clipboard", style = MaterialTheme.typography.labelLarge.copy( fontWeight = FontWeight.Bold, fontSize = 14.sp @@ -313,11 +316,10 @@ fun AutoTextDetailScreen( modifier = Modifier.fillMaxWidth() ) { Text( - text = "EXPANDED CONTENT", + text = "Isi Template", style = MaterialTheme.typography.labelMedium.copy( fontWeight = FontWeight.Bold, - letterSpacing = 1.sp, - fontSize = 12.sp + fontSize = 13.sp ), color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 4.dp, bottom = 8.dp) @@ -325,23 +327,23 @@ fun AutoTextDetailScreen( Card( modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp), + shape = RoundedCornerShape(12.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface ), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) ) { Box( modifier = Modifier .fillMaxWidth() - .padding(20.dp) + .padding(16.dp) ) { Text( text = data.body, style = MaterialTheme.typography.bodyLarge.copy( fontSize = 15.sp, - lineHeight = 24.sp + lineHeight = 22.sp ), color = MaterialTheme.colorScheme.onSurface ) @@ -357,7 +359,7 @@ fun AutoTextDetailScreen( icon = { Box( modifier = Modifier - .size(48.dp) + .size(44.dp) .clip(CircleShape) .background(FrogoStatusFailed.copy(alpha = 0.12f)), contentAlignment = Alignment.Center @@ -366,20 +368,20 @@ fun AutoTextDetailScreen( imageVector = Icons.Default.Delete, contentDescription = null, tint = FrogoStatusFailed, - modifier = Modifier.size(24.dp) + modifier = Modifier.size(22.dp) ) } }, title = { Text( - text = "Delete Template?", + text = "Hapus Template?", style = MaterialTheme.typography.titleMedium.copy( fontWeight = FontWeight.Bold ) ) }, text = { - Text("Are you sure you want to delete \"${data.title}\"? This action cannot be undone.") + Text("Apakah Anda yakin ingin menghapus template \"${data.title}\"? Tindakan ini tidak dapat dibatalkan.") }, confirmButton = { Button( @@ -390,10 +392,10 @@ fun AutoTextDetailScreen( colors = ButtonDefaults.buttonColors( containerColor = FrogoStatusFailed ), - shape = RoundedCornerShape(10.dp) + shape = RoundedCornerShape(8.dp) ) { Text( - text = "Delete", + text = "Hapus", color = Color.White, fontWeight = FontWeight.Bold ) @@ -403,7 +405,7 @@ fun AutoTextDetailScreen( TextButton( onClick = { showDeleteDialog = false } ) { - Text("Cancel") + Text("Batal") } }, shape = RoundedCornerShape(16.dp) diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextEditorScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextEditorScreen.kt index 959214f..508586b 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextEditorScreen.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextEditorScreen.kt @@ -2,6 +2,7 @@ package com.frogobox.appkeyboard.ui.autotext import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -81,13 +82,14 @@ fun AutoTextEditorScreen( bottomBar = { Surface( color = MaterialTheme.colorScheme.surface, - tonalElevation = 3.dp, - shadowElevation = 8.dp + tonalElevation = 0.dp, + shadowElevation = 0.dp, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) ) { Box( modifier = Modifier .fillMaxWidth() - .padding(16.dp) + .padding(horizontal = 16.dp, vertical = 12.dp) ) { Button( onClick = { @@ -97,8 +99,8 @@ fun AutoTextEditorScreen( }, modifier = Modifier .fillMaxWidth() - .height(52.dp), - shape = RoundedCornerShape(16.dp), + .height(48.dp), + shape = RoundedCornerShape(8.dp), enabled = isFormValid, colors = ButtonDefaults.buttonColors( containerColor = FrogoPrimary @@ -108,13 +110,13 @@ fun AutoTextEditorScreen( imageVector = Icons.Default.Check, contentDescription = null, tint = Color.White, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(18.dp) ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = if (isEditMode) "Update Template" else "Save Template", + text = if (isEditMode) "Simpan Perubahan" else "Simpan Template", style = MaterialTheme.typography.labelLarge.copy( - fontSize = 15.sp, + fontSize = 14.sp, fontWeight = FontWeight.SemiBold ), color = Color.White @@ -144,8 +146,8 @@ fun AutoTextEditorScreen( value = title, onValueChange = { if (it.length <= 50) title = it }, modifier = Modifier.fillMaxWidth(), - label = { Text("Shortcut Title") }, - placeholder = { Text("e.g. Greeting, BCA Rekening, Email") }, + label = { Text("Judul Pintasan") }, + placeholder = { Text("Contoh: Sapaan Pagi, Rekening BCA, Email Kantor") }, leadingIcon = { Icon( imageVector = Icons.AutoMirrored.Filled.ShortText, @@ -158,7 +160,7 @@ fun AutoTextEditorScreen( IconButton(onClick = { title = "" }) { Icon( imageVector = Icons.Default.Clear, - contentDescription = "Clear Title", + contentDescription = "Hapus Judul", tint = MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -169,17 +171,18 @@ fun AutoTextEditorScreen( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { - Text("Appears in keyboard quick bar") + Text("Tampil pada bilah cepat keyboard") Text("${title.length} / 50") } }, singleLine = true, - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(8.dp), keyboardOptions = KeyboardOptions( capitalization = KeyboardCapitalization.Sentences ), colors = OutlinedTextFieldDefaults.colors( focusedBorderColor = FrogoPrimary, + unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant, focusedLabelColor = FrogoPrimary, focusedContainerColor = MaterialTheme.colorScheme.surface, unfocusedContainerColor = MaterialTheme.colorScheme.surface @@ -191,8 +194,8 @@ fun AutoTextEditorScreen( value = body, onValueChange = { body = it }, modifier = Modifier.fillMaxWidth(), - label = { Text("Template Content") }, - placeholder = { Text("Enter the full text that will be inserted when tapped...") }, + label = { Text("Isi Template") }, + placeholder = { Text("Tulis teks lengkap yang akan disisipkan saat tombol diketuk...") }, leadingIcon = { Icon( imageVector = Icons.AutoMirrored.Filled.Notes, @@ -205,7 +208,7 @@ fun AutoTextEditorScreen( IconButton(onClick = { body = "" }) { Icon( imageVector = Icons.Default.Clear, - contentDescription = "Clear Content", + contentDescription = "Hapus Isi", tint = MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -216,17 +219,18 @@ fun AutoTextEditorScreen( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { - Text("Will be committed to input field") - Text("${body.length} characters") + Text("Akan langsung dimasukkan ke input teks") + Text("${body.length} karakter") } }, - minLines = 6, - shape = RoundedCornerShape(16.dp), + minLines = 5, + shape = RoundedCornerShape(8.dp), keyboardOptions = KeyboardOptions( capitalization = KeyboardCapitalization.Sentences ), colors = OutlinedTextFieldDefaults.colors( focusedBorderColor = FrogoPrimary, + unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant, focusedLabelColor = FrogoPrimary, focusedContainerColor = MaterialTheme.colorScheme.surface, unfocusedContainerColor = MaterialTheme.colorScheme.surface @@ -236,11 +240,12 @@ fun AutoTextEditorScreen( // Helper Guidance Card Card( modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors( - containerColor = FrogoPrimary.copy(alpha = 0.07f) + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f) ), - border = BorderStroke(1.dp, FrogoPrimary.copy(alpha = 0.20f)) + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) ) { Row( modifier = Modifier @@ -251,8 +256,8 @@ fun AutoTextEditorScreen( Box( modifier = Modifier .size(32.dp) - .clip(CircleShape) - .background(FrogoPrimary.copy(alpha = 0.16f)), + .clip(RoundedCornerShape(8.dp)) + .background(FrogoPrimary.copy(alpha = 0.12f)), contentAlignment = Alignment.Center ) { Icon( @@ -267,7 +272,7 @@ fun AutoTextEditorScreen( Column { Text( - text = "Keyboard Tip", + text = "Tips Keyboard", style = MaterialTheme.typography.labelMedium.copy( fontWeight = FontWeight.Bold ), @@ -275,7 +280,7 @@ fun AutoTextEditorScreen( ) Spacer(modifier = Modifier.height(2.dp)) Text( - text = "Short, distinct titles make it effortless to select snippets directly from the Frogo Keyboard while chatting or filling forms.", + text = "Judul yang ringkas dan jelas mempermudah pemilihan template langsung dari keyboard Frogo saat sedang mengetik pesan atau mengisi formulir.", style = MaterialTheme.typography.bodySmall.copy( lineHeight = 16.sp ), @@ -298,17 +303,17 @@ private fun LiveKeyboardPreviewCard( ) { Card( modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface ), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) ) { Column( modifier = Modifier .fillMaxWidth() - .padding(16.dp) + .padding(14.dp) ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -324,11 +329,10 @@ private fun LiveKeyboardPreviewCard( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "LIVE KEYBOARD PREVIEW", + text = "Pratinjau Keyboard", style = MaterialTheme.typography.labelMedium.copy( fontWeight = FontWeight.Bold, - letterSpacing = 0.8.sp, - fontSize = 11.sp + fontSize = 12.sp ), color = FrogoPrimary ) @@ -336,10 +340,11 @@ private fun LiveKeyboardPreviewCard( Surface( shape = RoundedCornerShape(6.dp), - color = FrogoPrimary.copy(alpha = 0.12f) + color = FrogoPrimary.copy(alpha = 0.12f), + border = BorderStroke(1.dp, FrogoPrimary.copy(alpha = 0.25f)) ) { Text( - text = "Interactive", + text = "Interaktif", style = MaterialTheme.typography.labelSmall.copy( fontWeight = FontWeight.SemiBold, fontSize = 10.sp, @@ -350,32 +355,36 @@ private fun LiveKeyboardPreviewCard( } } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(10.dp)) // Simulated Keyboard Item Box Box( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) - .padding(12.dp) + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f)) + .border( + BorderStroke(0.5.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)), + RoundedCornerShape(8.dp) + ) + .padding(10.dp) ) { Card( modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(10.dp), + shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface ), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f)) + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) ) { Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 14.dp, vertical = 10.dp) + .padding(horizontal = 12.dp, vertical = 10.dp) ) { Text( - text = if (title.isBlank()) "Template Title" else title, + text = if (title.isBlank()) "Judul Template" else title, style = MaterialTheme.typography.titleSmall.copy( fontWeight = FontWeight.Bold, fontSize = 14.sp @@ -388,7 +397,7 @@ private fun LiveKeyboardPreviewCard( Spacer(modifier = Modifier.height(2.dp)) Text( - text = if (body.isBlank()) "Expanded text preview will appear here..." else body, + text = if (body.isBlank()) "Isi teks template akan ditampilkan di sini..." else body, style = MaterialTheme.typography.bodySmall.copy( fontSize = 12.sp ), diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextScreen.kt index 8da8755..f171d83 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextScreen.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextScreen.kt @@ -6,6 +6,7 @@ import android.content.Context import android.widget.Toast import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -108,16 +109,16 @@ fun AutoTextScreen( onClick = onAddClick, containerColor = FrogoPrimary, contentColor = Color.White, - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(12.dp), icon = { Icon( imageVector = Icons.Default.Add, - contentDescription = "Add Template" + contentDescription = "Tambah Template" ) }, text = { Text( - text = "Add Template", + text = "Tambah Template", style = MaterialTheme.typography.labelLarge.copy( fontSize = 14.sp, fontWeight = FontWeight.SemiBold @@ -154,7 +155,7 @@ fun AutoTextScreen( modifier = Modifier.fillMaxWidth(), placeholder = { Text( - text = "Search templates by title or content...", + text = "Cari template berdasarkan judul atau isi...", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -162,7 +163,7 @@ fun AutoTextScreen( leadingIcon = { Icon( imageVector = Icons.Default.Search, - contentDescription = "Search", + contentDescription = "Cari", tint = FrogoPrimary ) }, @@ -171,17 +172,17 @@ fun AutoTextScreen( IconButton(onClick = { searchQuery = "" }) { Icon( imageVector = Icons.Default.Clear, - contentDescription = "Clear Search", + contentDescription = "Hapus Pencarian", tint = MaterialTheme.colorScheme.onSurfaceVariant ) } } }, singleLine = true, - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(8.dp), colors = OutlinedTextFieldDefaults.colors( focusedBorderColor = FrogoPrimary, - unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f), + unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant, focusedContainerColor = MaterialTheme.colorScheme.surface, unfocusedContainerColor = MaterialTheme.colorScheme.surface ) @@ -202,8 +203,10 @@ fun AutoTextScreen( } autoTextList.isEmpty() -> { FrogoEmptyView( - title = "NO AUTO TEXT FOUND", - subtitle = "Tap 'Add Template' below to create a quick template.", + title = "Belum Ada Auto Text", + subtitle = "Buat template pesan cepat untuk mempercepat pengetikan Anda.", + actionButtonText = "Tambah Template", + onActionClick = onAddClick, modifier = Modifier.align(Alignment.Center) ) } @@ -223,7 +226,7 @@ fun AutoTextScreen( top = 4.dp, bottom = 88.dp // Space for Extended FAB ), - verticalArrangement = Arrangement.spacedBy(12.dp) + verticalArrangement = Arrangement.spacedBy(10.dp) ) { items(filteredList, key = { it.id }) { item -> AutoTextCard( @@ -249,11 +252,12 @@ private fun AutoTextHeaderBanner( ) { Card( modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors( - containerColor = FrogoPrimary.copy(alpha = 0.08f) + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f) ), - border = BorderStroke(1.dp, FrogoPrimary.copy(alpha = 0.22f)) + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) ) { Row( modifier = Modifier @@ -268,14 +272,14 @@ private fun AutoTextHeaderBanner( ) { Box( modifier = Modifier - .size(40.dp) - .clip(CircleShape) - .background(FrogoPrimary.copy(alpha = 0.16f)), + .size(38.dp) + .clip(RoundedCornerShape(8.dp)) + .background(FrogoPrimary.copy(alpha = 0.12f)), contentAlignment = Alignment.Center ) { Icon( imageVector = Icons.AutoMirrored.Filled.ShortText, - contentDescription = "Templates Icon", + contentDescription = "Ikon Template", tint = FrogoPrimary, modifier = Modifier.size(22.dp) ) @@ -285,7 +289,7 @@ private fun AutoTextHeaderBanner( Column { Text( - text = "Quick Templates", + text = "Template Cepat", style = MaterialTheme.typography.titleMedium.copy( fontWeight = FontWeight.Bold, fontSize = 15.sp @@ -293,7 +297,7 @@ private fun AutoTextHeaderBanner( color = MaterialTheme.colorScheme.onSurface ) Text( - text = "Instant shortcuts on your keyboard", + text = "Pintasan teks instan pada keyboard", style = MaterialTheme.typography.bodySmall.copy( fontSize = 12.sp ), @@ -303,11 +307,12 @@ private fun AutoTextHeaderBanner( } Surface( - shape = RoundedCornerShape(10.dp), - color = FrogoPrimary.copy(alpha = 0.16f) + shape = RoundedCornerShape(8.dp), + color = FrogoPrimary.copy(alpha = 0.12f), + border = BorderStroke(1.dp, FrogoPrimary.copy(alpha = 0.25f)) ) { Text( - text = if (isSearching) "$filteredCount / $totalCount" else "$totalCount Saved", + text = if (isSearching) "$filteredCount / $totalCount" else "$totalCount Tersimpan", style = MaterialTheme.typography.labelSmall.copy( fontWeight = FontWeight.Bold, fontSize = 11.sp, @@ -331,17 +336,17 @@ private fun AutoTextCard( modifier = modifier .fillMaxWidth() .clickable { onClick() }, - shape = RoundedCornerShape(16.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), + shape = RoundedCornerShape(12.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface ), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) ) { Column( modifier = Modifier .fillMaxWidth() - .padding(16.dp) + .padding(14.dp) ) { Row( modifier = Modifier.fillMaxWidth(), @@ -354,8 +359,8 @@ private fun AutoTextCard( ) { Box( modifier = Modifier - .size(36.dp) - .clip(RoundedCornerShape(10.dp)) + .size(34.dp) + .clip(RoundedCornerShape(8.dp)) .background(FrogoPrimary.copy(alpha = 0.12f)), contentAlignment = Alignment.Center ) { @@ -363,17 +368,17 @@ private fun AutoTextCard( imageVector = Icons.AutoMirrored.Filled.Notes, contentDescription = null, tint = FrogoPrimary, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(18.dp) ) } - Spacer(modifier = Modifier.width(12.dp)) + Spacer(modifier = Modifier.width(10.dp)) Text( text = item.title, style = MaterialTheme.typography.titleMedium.copy( fontWeight = FontWeight.Bold, - fontSize = 16.sp + fontSize = 15.sp ), color = MaterialTheme.colorScheme.onSurface, maxLines = 1, @@ -383,25 +388,29 @@ private fun AutoTextCard( IconButton( onClick = onCopy, - modifier = Modifier.size(36.dp) + modifier = Modifier.size(34.dp) ) { Icon( imageVector = Icons.Default.ContentCopy, - contentDescription = "Copy text", + contentDescription = "Salin teks", tint = FrogoPrimary, modifier = Modifier.size(18.dp) ) } } - Spacer(modifier = Modifier.height(10.dp)) + Spacer(modifier = Modifier.height(8.dp)) // Styled Snippet Container Box( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)) + .clip(RoundedCornerShape(8.dp)) .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f)) + .border( + BorderStroke(0.5.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)), + RoundedCornerShape(8.dp) + ) .padding(horizontal = 12.dp, vertical = 8.dp) ) { Text( @@ -434,23 +443,23 @@ private fun EmptySearchState( ) { Box( modifier = Modifier - .size(64.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.surfaceVariant), + .size(56.dp) + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f)), contentAlignment = Alignment.Center ) { Icon( imageVector = Icons.Default.SearchOff, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(32.dp) + modifier = Modifier.size(28.dp) ) } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(14.dp)) Text( - text = "No templates found", + text = "Template tidak ditemukan", style = MaterialTheme.typography.titleMedium.copy( fontWeight = FontWeight.Bold ), @@ -460,7 +469,7 @@ private fun EmptySearchState( Spacer(modifier = Modifier.height(4.dp)) Text( - text = "No template matches \"$query\"", + text = "Tidak ada template yang cocok dengan \"$query\"", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -469,7 +478,7 @@ private fun EmptySearchState( TextButton(onClick = onClearSearch) { Text( - text = "Clear Search", + text = "Hapus Pencarian", color = FrogoPrimary, fontWeight = FontWeight.SemiBold ) diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboardScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboardScreen.kt index ab36f22..c45447a 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboardScreen.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/form/FormKeyboardScreen.kt @@ -1,5 +1,6 @@ package com.frogobox.appkeyboard.ui.keyboard.form +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -78,9 +79,10 @@ fun FormKeyboardScreen( keyboardActions = KeyboardActions( onNext = { focusManager.moveFocus(FocusDirection.Down) } ), - shape = RoundedCornerShape(10.dp), + shape = RoundedCornerShape(8.dp), colors = OutlinedTextFieldDefaults.colors( focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant, focusedLabelColor = MaterialTheme.colorScheme.primary ), modifier = Modifier.fillMaxWidth() @@ -100,9 +102,10 @@ fun FormKeyboardScreen( keyboardActions = KeyboardActions( onNext = { focusManager.moveFocus(FocusDirection.Down) } ), - shape = RoundedCornerShape(10.dp), + shape = RoundedCornerShape(8.dp), colors = OutlinedTextFieldDefaults.colors( focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant, focusedLabelColor = MaterialTheme.colorScheme.primary ), modifier = Modifier.fillMaxWidth() @@ -123,9 +126,10 @@ fun FormKeyboardScreen( keyboardActions = KeyboardActions( onDone = { focusManager.clearFocus() } ), - shape = RoundedCornerShape(10.dp), + shape = RoundedCornerShape(8.dp), colors = OutlinedTextFieldDefaults.colors( focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant, focusedLabelColor = MaterialTheme.colorScheme.primary ), modifier = Modifier.fillMaxWidth() @@ -146,7 +150,8 @@ fun FormKeyboardScreen( modifier = Modifier .weight(1f) .height(44.dp), - shape = RoundedCornerShape(12.dp) + shape = RoundedCornerShape(8.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) ) { Text("Clear", fontSize = 12.sp) } @@ -167,7 +172,7 @@ fun FormKeyboardScreen( modifier = Modifier .weight(2f) .height(44.dp), - shape = RoundedCornerShape(12.dp), + shape = RoundedCornerShape(8.dp), colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.primary ) diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/movie/MovieKeyboardScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/movie/MovieKeyboardScreen.kt index 673750d..18498db 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/movie/MovieKeyboardScreen.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/movie/MovieKeyboardScreen.kt @@ -1,6 +1,7 @@ package com.frogobox.appkeyboard.ui.keyboard.movie import java.util.Locale +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -90,7 +91,7 @@ fun MovieKeyboardScreen( Card( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)) + .clip(RoundedCornerShape(8.dp)) .clickable { val output = "Movie: ${movie.title ?: ""} - ${movie.release_date ?: ""} \n " + "Overview: ${movie.overview ?: ""} \n " + @@ -100,10 +101,11 @@ fun MovieKeyboardScreen( "Terima Kasih" onCommitText(output) }, - shape = RoundedCornerShape(10.dp), + shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f) - ) + ), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f)) ) { Column(modifier = Modifier.fillMaxWidth()) { // Poster diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/news/NewsKeyboardScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/news/NewsKeyboardScreen.kt index 9850f4b..bdfec54 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/news/NewsKeyboardScreen.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/news/NewsKeyboardScreen.kt @@ -1,5 +1,6 @@ package com.frogobox.appkeyboard.ui.keyboard.news +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -79,7 +80,7 @@ fun NewsKeyboardScreen( Card( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)) + .clip(RoundedCornerShape(8.dp)) .clickable { val output = "${article.title}\n" + "${article.author ?: "News"}\n" + @@ -91,10 +92,11 @@ fun NewsKeyboardScreen( "Terima Kasih" onCommitText(output) }, - shape = RoundedCornerShape(10.dp), + shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f) - ) + ), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f)) ) { Row( modifier = Modifier diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardFeatureHeader.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardFeatureHeader.kt index 3e2dda9..7cd3880 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardFeatureHeader.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardFeatureHeader.kt @@ -5,14 +5,17 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape @@ -23,17 +26,23 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.frogobox.appkeyboard.model.KeyboardFeatureModel +import com.frogobox.appkeyboard.model.KeyboardFeatureType +import com.frogobox.appkeyboard.ui.theme.compose.FrogoKeyboardTheme /** - * Modern Jetpack Compose Feature Header Bar replacing legacy RecyclerView and ItemKeyboardHeaderBinding. - * Renders horizontally scrollable feature shortcuts according to Material 3 tokens and Angel's design specification. + * Modern Jetpack Compose Feature Header Bar. + * Renders feature shortcuts with clean, balanced positioning: + * - When items fit comfortably (<= 5 items), items are evenly distributed across full width with equal columns. + * - When there are more items, renders a smooth horizontal scrollable row with uniform item slots. */ @Composable fun KeyboardFeatureHeader( @@ -43,35 +52,70 @@ fun KeyboardFeatureHeader( ) { if (features.isEmpty()) return - Column( + BoxWithConstraints( modifier = modifier .fillMaxWidth() .height(56.dp) .background(MaterialTheme.colorScheme.surface) ) { - LazyRow( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - horizontalArrangement = Arrangement.spacedBy(4.dp), - contentPadding = PaddingValues(horizontal = 8.dp, vertical = 3.dp), - verticalAlignment = Alignment.CenterVertically + val totalWidth = maxWidth + val minComfortableWidth = 70.dp + val canFitEvenly = features.size <= 5 && (totalWidth / features.size) >= minComfortableWidth + + Column( + modifier = Modifier.fillMaxSize() ) { - items( - items = features, - key = { it.id } - ) { feature -> - KeyboardFeatureHeaderItem( - feature = feature, - onClick = { onFeatureClick(feature) } - ) + if (canFitEvenly) { + // Symmetrical equal-width distribution across the full keyboard width + Row( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 4.dp, vertical = 3.dp), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + features.forEach { feature -> + KeyboardFeatureHeaderItem( + feature = feature, + onClick = { onFeatureClick(feature) }, + modifier = Modifier + .weight(1f) + .fillMaxHeight() + ) + } + } + } else { + // Horizontally scrollable row with uniform item width for consistent spacing & peeking indicator + val itemWidth = (totalWidth / 4.25f).coerceIn(72.dp, 92.dp) + LazyRow( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + horizontalArrangement = Arrangement.spacedBy(4.dp), + contentPadding = PaddingValues(horizontal = 6.dp, vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically + ) { + items( + items = features, + key = { it.id } + ) { feature -> + KeyboardFeatureHeaderItem( + feature = feature, + onClick = { onFeatureClick(feature) }, + modifier = Modifier + .width(itemWidth) + .fillMaxHeight() + ) + } + } } - } - HorizontalDivider( - thickness = 0.8.dp, - color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.25f) - ) + HorizontalDivider( + thickness = 0.8.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.25f) + ) + } } } @@ -83,30 +127,73 @@ private fun KeyboardFeatureHeaderItem( ) { Column( modifier = modifier - .defaultMinSize(minWidth = 72.dp) - .height(48.dp) .clip(RoundedCornerShape(8.dp)) .clickable(onClick = onClick) - .padding(horizontal = 4.dp, vertical = 2.dp), + .padding(horizontal = 4.dp, vertical = 3.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - Image( - painter = painterResource(id = feature.icon), - contentDescription = feature.text, - modifier = Modifier.size(24.dp) - ) + Box( + modifier = Modifier.size(24.dp), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(id = feature.icon), + contentDescription = feature.text, + modifier = Modifier.size(22.dp), + contentScale = ContentScale.Fit + ) + } Text( text = feature.text, - fontSize = 10.sp, + fontSize = 9.5.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface, maxLines = 1, softWrap = false, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, - modifier = Modifier.padding(top = 2.dp) + modifier = Modifier + .fillMaxWidth() + .padding(top = 2.dp) + ) + } +} + +@Preview(name = "Header 4 Items Even Distribution", showBackground = true, widthDp = 360) +@Composable +private fun KeyboardFeatureHeaderFourItemsPreview() { + FrogoKeyboardTheme { + val sampleFeatures = listOf( + KeyboardFeatureType.AUTO_TEXT.mapToModel(), + KeyboardFeatureType.MOVIE.mapToModel(), + KeyboardFeatureType.CHANGE_KEYBOARD.mapToModel(), + KeyboardFeatureType.SETTING.mapToModel() + ) + KeyboardFeatureHeader( + features = sampleFeatures, + onFeatureClick = {} + ) + } +} + +@Preview(name = "Header Scrollable Many Items", showBackground = true, widthDp = 360) +@Composable +private fun KeyboardFeatureHeaderManyItemsPreview() { + FrogoKeyboardTheme { + val sampleFeatures = listOf( + KeyboardFeatureType.AUTO_TEXT.mapToModel(), + KeyboardFeatureType.MOVIE.mapToModel(), + KeyboardFeatureType.NEWS.mapToModel(), + KeyboardFeatureType.WEB.mapToModel(), + KeyboardFeatureType.FORM.mapToModel(), + KeyboardFeatureType.CHANGE_KEYBOARD.mapToModel(), + KeyboardFeatureType.SETTING.mapToModel() + ) + KeyboardFeatureHeader( + features = sampleFeatures, + onFeatureClick = {} ) } } diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardSuggestionBar.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardSuggestionBar.kt index 2e60aa4..9c3b6a0 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardSuggestionBar.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/root/KeyboardSuggestionBar.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.frogobox.appkeyboard.R import com.frogobox.appkeyboard.suggestion.SuggestionResult +import com.frogobox.appkeyboard.ui.theme.compose.FrogoStatusSuccess /** * Candidate types corresponding to the 3 predictive word strip sections. @@ -136,12 +137,12 @@ fun KeyboardSuggestionBar( modifier = Modifier .weight(1.2f) .height(32.dp) - .clip(RoundedCornerShape(16.dp)) + .clip(RoundedCornerShape(8.dp)) .clickable { onCandidateSelected(result.predictedWord, CandidateType.PREDICTED_WORD) }, - shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.7f), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.35f)), - shadowElevation = 1.dp + shape = RoundedCornerShape(8.dp), + color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.4f)), + shadowElevation = 0.dp ) { Box( modifier = Modifier @@ -192,7 +193,7 @@ fun KeyboardSuggestionBar( text = result.autoCorrectWord, fontSize = 13.sp, fontWeight = FontWeight.Medium, - color = Color(0xFF2E7D32), + color = FrogoStatusSuccess, maxLines = 1, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboardScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboardScreen.kt index 890f252..fe4c9d0 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboardScreen.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/templatetext/TemplateTextKeyboardScreen.kt @@ -1,5 +1,6 @@ package com.frogobox.appkeyboard.ui.keyboard.templatetext +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -113,10 +114,16 @@ fun TemplateTextKeyboardScreen( fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal ) }, - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(8.dp), colors = FilterChipDefaults.filterChipColors( selectedContainerColor = MaterialTheme.colorScheme.primaryContainer, selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer + ), + border = FilterChipDefaults.filterChipBorder( + enabled = true, + selected = isSelected, + borderColor = MaterialTheme.colorScheme.outlineVariant, + selectedBorderColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f) ) ) } @@ -145,7 +152,8 @@ fun TemplateTextKeyboardScreen( colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f) ), - shape = RoundedCornerShape(8.dp) + shape = RoundedCornerShape(8.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f)) ) { Text( text = template.text, diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/language/KeyboardLanguageScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/language/KeyboardLanguageScreen.kt index 2daf85d..ce9a9be 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/language/KeyboardLanguageScreen.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/language/KeyboardLanguageScreen.kt @@ -198,14 +198,14 @@ fun KeyboardLanguageScreen( verticalArrangement = Arrangement.spacedBy(12.dp) ) { Text( - text = "Are you sure you want to switch your active typing layout?", + text = "Apakah Anda yakin ingin mengganti tata letak keyboard aktif?", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) // Selected Language Preview Box Surface( - shape = RoundedCornerShape(12.dp), + shape = RoundedCornerShape(8.dp), color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), modifier = Modifier.fillMaxWidth() ) { @@ -219,7 +219,7 @@ fun KeyboardLanguageScreen( modifier = Modifier .size(40.dp) .clip(CircleShape) - .background(FrogoPrimary.copy(alpha = 0.2f)), + .background(FrogoPrimary.copy(alpha = 0.15f)), contentAlignment = Alignment.Center ) { Text( @@ -261,10 +261,10 @@ fun KeyboardLanguageScreen( colors = ButtonDefaults.buttonColors( containerColor = FrogoPrimary ), - shape = RoundedCornerShape(10.dp) + shape = RoundedCornerShape(8.dp) ) { Text( - text = "Apply Layout", + text = "Terapkan Layout", fontWeight = FontWeight.Bold ) } @@ -273,10 +273,10 @@ fun KeyboardLanguageScreen( TextButton( onClick = { selectedLanguageForDialog = null } ) { - Text("Cancel") + Text("Batal") } }, - shape = RoundedCornerShape(18.dp) + shape = RoundedCornerShape(16.dp) ) } } @@ -312,11 +312,11 @@ private fun ActiveLanguageHeroCard( ) { Card( modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(18.dp), + shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors( - containerColor = FrogoPrimary.copy(alpha = 0.08f) + containerColor = MaterialTheme.colorScheme.surface ), - border = BorderStroke(1.5.dp, FrogoPrimary.copy(alpha = 0.35f)), + border = BorderStroke(1.dp, FrogoPrimary.copy(alpha = 0.5f)), elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) ) { Column( @@ -332,8 +332,8 @@ private fun ActiveLanguageHeroCard( horizontalArrangement = Arrangement.SpaceBetween ) { Surface( - shape = RoundedCornerShape(8.dp), - color = FrogoStatusSuccess.copy(alpha = 0.15f) + shape = RoundedCornerShape(6.dp), + color = FrogoStatusSuccess.copy(alpha = 0.12f) ) { Row( modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), @@ -347,7 +347,7 @@ private fun ActiveLanguageHeroCard( .background(FrogoStatusSuccess) ) Text( - text = "CURRENT ACTIVE", + text = "LAYOUT AKTIF", style = MaterialTheme.typography.labelSmall.copy( fontWeight = FontWeight.Bold, fontSize = 10.sp, @@ -358,7 +358,7 @@ private fun ActiveLanguageHeroCard( } Text( - text = "Default Input Method", + text = "Input Default", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -448,10 +448,10 @@ private fun SearchAndFilterHeader( value = query, onValueChange = onQueryChange, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(8.dp), placeholder = { Text( - text = "Search language, code, or layout...", + text = "Cari bahasa, kode, atau tata letak...", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) ) @@ -459,7 +459,7 @@ private fun SearchAndFilterHeader( leadingIcon = { Icon( imageVector = Icons.Default.Search, - contentDescription = "Search", + contentDescription = "Cari", tint = FrogoPrimary, modifier = Modifier.size(20.dp) ) @@ -469,7 +469,7 @@ private fun SearchAndFilterHeader( IconButton(onClick = { onQueryChange("") }) { Icon( imageVector = Icons.Default.Close, - contentDescription = "Clear Search", + contentDescription = "Hapus Pencarian", tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(20.dp) ) @@ -479,7 +479,7 @@ private fun SearchAndFilterHeader( singleLine = true, colors = OutlinedTextFieldDefaults.colors( focusedBorderColor = FrogoPrimary, - unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f), + unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant, focusedContainerColor = MaterialTheme.colorScheme.surface, unfocusedContainerColor = MaterialTheme.colorScheme.surface ) @@ -494,7 +494,7 @@ private fun SearchAndFilterHeader( horizontalArrangement = Arrangement.SpaceBetween ) { Text( - text = if (query.isBlank()) "Available Languages" else "Search Results", + text = if (query.isBlank()) "Daftar Bahasa Tersedia" else "Hasil Pencarian", style = MaterialTheme.typography.titleSmall.copy( fontWeight = FontWeight.Bold ), @@ -502,11 +502,11 @@ private fun SearchAndFilterHeader( ) Surface( - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + shape = RoundedCornerShape(6.dp), + color = MaterialTheme.colorScheme.surfaceVariant ) { Text( - text = if (query.isBlank()) "$totalLanguages available" else "$filteredCount found", + text = if (query.isBlank()) "$totalLanguages bahasa" else "$filteredCount ditemukan", style = MaterialTheme.typography.labelSmall.copy( fontWeight = FontWeight.Medium ), @@ -531,11 +531,10 @@ private fun LanguageCard( Card( modifier = modifier .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) .clickable { onClick() }, - shape = RoundedCornerShape(16.dp), - elevation = CardDefaults.cardElevation( - defaultElevation = if (isSelected) 3.dp else 1.dp - ), + shape = RoundedCornerShape(12.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), colors = CardDefaults.cardColors( containerColor = if (isSelected) { FrogoPrimary.copy(alpha = 0.04f) @@ -544,11 +543,11 @@ private fun LanguageCard( } ), border = BorderStroke( - width = if (isSelected) 2.dp else 1.dp, + width = if (isSelected) 1.5.dp else 1.dp, color = if (isSelected) { FrogoPrimary } else { - MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.45f) + MaterialTheme.colorScheme.outlineVariant } ) ) { @@ -657,11 +656,11 @@ private fun LanguageCard( horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Surface( - shape = RoundedCornerShape(8.dp), - color = FrogoStatusSuccess.copy(alpha = 0.15f) + shape = RoundedCornerShape(6.dp), + color = FrogoStatusSuccess.copy(alpha = 0.12f) ) { Text( - text = "ACTIVE", + text = "Aktif", style = MaterialTheme.typography.labelSmall.copy( fontWeight = FontWeight.Bold, fontSize = 10.sp, diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/main/MainScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/main/MainScreen.kt index 3e18982..84c56d7 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/main/MainScreen.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/main/MainScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -59,23 +60,23 @@ enum class KeyboardStatus( val icon: ImageVector ) { ACTIVE( - title = "Frogo Keyboard Active", - description = "Keyboard is activated and set as default input method.", - badge = "ACTIVE", + title = "Keyboard Siap Digunakan", + description = "Frogo Keyboard aktif dan terpilih sebagai metode input utama.", + badge = "AKTIF", color = FrogoStatusSuccess, icon = Icons.Default.CheckCircle ), NOT_DEFAULT( - title = "Not Default Keyboard", - description = "Keyboard is enabled, but another keyboard is currently selected.", - badge = "SELECT FROGO", + title = "Bukan Keyboard Utama", + description = "Keyboard sudah aktif di sistem, namun belum dipilih sebagai input default.", + badge = "PILIH FROGO", color = FrogoStatusWarning, icon = Icons.Default.Warning ), NOT_ENABLED( - title = "Frogo Keyboard Inactive", - description = "Enable Frogo Keyboard in Android system settings to get started.", - badge = "NOT ENABLED", + title = "Keyboard Belum Aktif", + description = "Aktifkan Frogo Keyboard melalui Pengaturan Sistem Android.", + badge = "BELUM AKTIF", color = FrogoStatusFailed, icon = Icons.Default.Info ) @@ -110,26 +111,27 @@ fun MainScreen( .fillMaxSize() .verticalScroll(scrollState) .padding(paddingValues) - .padding(horizontal = 20.dp, vertical = 12.dp), + .padding(horizontal = 16.dp, vertical = 12.dp), horizontalAlignment = Alignment.Start ) { // Header Description Text( - text = "Enable and set as default keyboard to start typing with custom features and personalized tools.", + text = "Konfigurasi metode input, kelola pintasan teks cepat, suara mekanik, dan personalisasi tema keyboard Anda.", style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = 20.sp ) Spacer(modifier = Modifier.height(16.dp)) - // Reactive Status Card + // Tactile Status Card (Hardware-inspired Precision System Status) Card( modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors( - containerColor = status.color.copy(alpha = 0.12f) + containerColor = MaterialTheme.colorScheme.surface ), - border = BorderStroke(1.dp, status.color.copy(alpha = 0.35f)), + border = BorderStroke(1.dp, status.color.copy(alpha = 0.5f)), elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) ) { Row( @@ -140,16 +142,16 @@ fun MainScreen( ) { Box( modifier = Modifier - .size(42.dp) + .size(40.dp) .clip(CircleShape) - .background(status.color.copy(alpha = 0.2f)), + .background(status.color.copy(alpha = 0.12f)), contentAlignment = Alignment.Center ) { Icon( imageVector = status.icon, contentDescription = null, tint = status.color, - modifier = Modifier.size(24.dp) + modifier = Modifier.size(22.dp) ) } @@ -164,15 +166,15 @@ fun MainScreen( Text( text = status.title, style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.Bold, + fontWeight = FontWeight.SemiBold, fontSize = 15.sp ), - color = status.color + color = MaterialTheme.colorScheme.onSurface ) Surface( shape = RoundedCornerShape(6.dp), - color = status.color.copy(alpha = 0.2f) + color = status.color.copy(alpha = 0.12f) ) { Text( text = status.badge, @@ -181,17 +183,18 @@ fun MainScreen( fontSize = 10.sp ), color = status.color, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + modifier = Modifier.padding(horizontal = 7.dp, vertical = 2.dp) ) } } - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(3.dp)) Text( text = status.description, style = MaterialTheme.typography.bodySmall.copy( - fontSize = 12.sp + fontSize = 12.sp, + lineHeight = 16.sp ), color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -201,15 +204,15 @@ fun MainScreen( Spacer(modifier = Modifier.height(16.dp)) - // Activation Steps Row (Step 1: Settings, Step 2: Switch Keyboard) + // Activation Steps Row (Tactile Hardware Steps) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp) ) { ActivationCard( stepNumber = "1", - title = "Enable Keyboard", - subtitle = "System Settings", + title = "Aktifkan Keyboard", + subtitle = "Pengaturan Sistem", iconRes = R.drawable.ic_menu_setting, onClick = onGoToSettings, modifier = Modifier.weight(1f) @@ -217,19 +220,19 @@ fun MainScreen( ActivationCard( stepNumber = "2", - title = "Set Default", - subtitle = "Switch Keyboard", + title = "Pilih Default", + subtitle = "Ganti Input Aktif", iconRes = R.drawable.ic_menu_keyboard, onClick = onChangeKeyboard, modifier = Modifier.weight(1f) ) } - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(22.dp)) // Section Header Text( - text = "Features & Customization", + text = "Fitur dan Personalisasi", style = MaterialTheme.typography.titleMedium.copy( fontWeight = FontWeight.Bold, fontSize = 16.sp @@ -237,51 +240,51 @@ fun MainScreen( color = MaterialTheme.colorScheme.onBackground ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(10.dp)) - // Feature Menu List (Material 3 Cards with Icon, Subtitle, and Chevron) + // Feature Menu List (Tactile 1dp Border Cards) Column( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(10.dp) ) { MainMenuItemCard( title = "Auto Text", - subtitle = "Quick snippets & repetitive text templates", + subtitle = "Template teks cepat dan pintasan ketikan berulang", iconRes = R.drawable.ic_menu_auto_text, onClick = onNavigateAutoText ) MainMenuItemCard( - title = "Toggle Function", - subtitle = "Keyboard shortcuts & toolbar switches", + title = "Toggle Fitur", + subtitle = "Kustomisasi tombol pintas dan toolbar keyboard", iconRes = R.drawable.ic_menu_form, onClick = onNavigateToggle ) MainMenuItemCard( - title = "Sound & Haptics", - subtitle = "Mechanical switch sounds & key vibration", + title = "Suara dan Haptic", + subtitle = "Suara switch mechanical keyboard dan feedback getaran", iconRes = R.drawable.ic_menu_sound, onClick = onNavigateSound ) MainMenuItemCard( - title = "Keyboard Language", - subtitle = "Multilingual input languages & layouts", + title = "Bahasa Keyboard", + subtitle = "Dukungan multibahasa dan layout QWERTY internasional", iconRes = R.drawable.ic_menu_website, onClick = onNavigateLanguage ) MainMenuItemCard( - title = "Keyboard Theme", - subtitle = "Personalized colors, themes & wallpapers", + title = "Tema Keyboard", + subtitle = "Pilihan tema warna solid, wallpaper, dan preview interaktif", iconRes = R.drawable.ic_menu_ps_game, onClick = onNavigateTheme ) MainMenuItemCard( - title = "Do Some Test", - subtitle = "Interactive typing test & keyboard sandbox", + title = "Playground Uji Coba", + subtitle = "Uji kecepatan ketik (WPM), akurasi, dan sandbox keyboard", iconRes = R.drawable.ic_menu_keyboard, onClick = onNavigateTest ) @@ -303,18 +306,19 @@ private fun ActivationCard( ) { Card( modifier = modifier + .clip(RoundedCornerShape(12.dp)) .clickable(onClick = onClick), - shape = RoundedCornerShape(16.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp), + shape = RoundedCornerShape(12.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface ), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) ) { Column( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .padding(14.dp), horizontalAlignment = Alignment.Start ) { Row( @@ -325,15 +329,15 @@ private fun ActivationCard( Image( painter = painterResource(id = iconRes), contentDescription = title, - modifier = Modifier.size(36.dp) + modifier = Modifier.size(32.dp) ) Surface( shape = RoundedCornerShape(6.dp), - color = FrogoPrimary.copy(alpha = 0.1f) + color = FrogoPrimary.copy(alpha = 0.12f) ) { Text( - text = "STEP $stepNumber", + text = "LANGKAH $stepNumber", style = MaterialTheme.typography.labelSmall.copy( fontWeight = FontWeight.Bold, fontSize = 10.sp @@ -344,12 +348,12 @@ private fun ActivationCard( } } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(10.dp)) Text( text = title, style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Bold, + fontWeight = FontWeight.SemiBold, fontSize = 14.sp ), color = MaterialTheme.colorScheme.onSurface @@ -379,24 +383,25 @@ private fun MainMenuItemCard( Card( modifier = modifier .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) .clickable(onClick = onClick), - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface ), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) ) { Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 14.dp), + .padding(horizontal = 14.dp, vertical = 14.dp), verticalAlignment = Alignment.CenterVertically ) { Image( painter = painterResource(id = iconRes), contentDescription = title, - modifier = Modifier.size(36.dp) + modifier = Modifier.size(32.dp) ) Spacer(modifier = Modifier.width(14.dp)) @@ -416,7 +421,8 @@ private fun MainMenuItemCard( Text( text = subtitle, style = MaterialTheme.typography.bodySmall.copy( - fontSize = 12.sp + fontSize = 12.sp, + lineHeight = 16.sp ), color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/sound/SoundScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/sound/SoundScreen.kt index 1023e73..5c3a54d 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/sound/SoundScreen.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/sound/SoundScreen.kt @@ -103,17 +103,16 @@ fun SoundScreen( item(span = { GridItemSpan(maxLineSpan) }) { Column(modifier = Modifier.padding(top = 8.dp, bottom = 2.dp)) { Text( - text = "MECHANICAL SWITCH PROFILES (MECHVIBES)", - style = MaterialTheme.typography.labelMedium.copy( + text = "Profil Suara Switch Mekanik", + style = MaterialTheme.typography.titleSmall.copy( fontWeight = FontWeight.Bold, - fontSize = 12.sp, - letterSpacing = 0.8.sp + fontSize = 14.sp ), color = MaterialTheme.colorScheme.primary ) Spacer(modifier = Modifier.height(2.dp)) Text( - text = "Authentic tactile & acoustic mechanical switch models", + text = "Model suara dan karakter akustik mechanical switch autentik", style = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp), color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -138,11 +137,10 @@ fun SoundScreen( item(span = { GridItemSpan(maxLineSpan) }) { Column(modifier = Modifier.padding(top = 10.dp, bottom = 2.dp)) { Text( - text = "VOLUME & HAPTIC FEEDBACK", - style = MaterialTheme.typography.labelMedium.copy( + text = "Volume dan Feedback Haptic", + style = MaterialTheme.typography.titleSmall.copy( fontWeight = FontWeight.Bold, - fontSize = 12.sp, - letterSpacing = 0.8.sp + fontSize = 14.sp ), color = MaterialTheme.colorScheme.primary ) @@ -203,11 +201,11 @@ private fun MasterSoundCard( Card( modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(18.dp), + shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors(containerColor = cardBackground), border = BorderStroke( width = 1.dp, - color = if (soundEnabled) primaryColor.copy(alpha = 0.4f) else MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f) + color = if (soundEnabled) primaryColor.copy(alpha = 0.4f) else MaterialTheme.colorScheme.outlineVariant ), elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) ) { @@ -275,7 +273,7 @@ private fun SoundProfileCard( ) { val primaryColor = MaterialTheme.colorScheme.primary val borderColor by animateColorAsState( - targetValue = if (isSelected) primaryColor else MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f), + targetValue = if (isSelected) primaryColor else MaterialTheme.colorScheme.outlineVariant, label = "border" ) val containerColor by animateColorAsState( @@ -286,9 +284,9 @@ private fun SoundProfileCard( Card( modifier = modifier .fillMaxWidth() - .clip(RoundedCornerShape(16.dp)) + .clip(RoundedCornerShape(12.dp)) .clickable(onClick = onClick), - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors(containerColor = containerColor), border = BorderStroke(if (isSelected) 1.5.dp else 1.dp, borderColor), elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) @@ -304,21 +302,21 @@ private fun SoundProfileCard( verticalAlignment = Alignment.CenterVertically ) { Surface( - shape = RoundedCornerShape(8.dp), + shape = RoundedCornerShape(6.dp), color = if (isSelected) FrogoPrimary.copy(alpha = 0.15f) else MaterialTheme.colorScheme.surfaceVariant ) { Text( text = when (profile) { - MechanicalSoundType.CHERRY_MX_BLUE -> "CLICKY" - MechanicalSoundType.CHERRY_MX_BROWN -> "TACTILE" - MechanicalSoundType.CHERRY_MX_RED -> "LINEAR" - MechanicalSoundType.TYPEWRITER -> "VINTAGE" - MechanicalSoundType.SYSTEM_CLICK -> "SYSTEM" - MechanicalSoundType.OFF -> "MUTE" + MechanicalSoundType.CHERRY_MX_BLUE -> "Clicky" + MechanicalSoundType.CHERRY_MX_BROWN -> "Tactile" + MechanicalSoundType.CHERRY_MX_RED -> "Linear" + MechanicalSoundType.TYPEWRITER -> "Vintage" + MechanicalSoundType.SYSTEM_CLICK -> "System" + MechanicalSoundType.OFF -> "Mute" }, style = MaterialTheme.typography.labelSmall.copy( - fontWeight = FontWeight.Bold, - fontSize = 9.sp + fontWeight = FontWeight.SemiBold, + fontSize = 10.sp ), color = if (isSelected) FrogoPrimary else MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) @@ -370,9 +368,9 @@ private fun VolumeControlCard( ) { Card( modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f)), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) ) { Column( @@ -425,9 +423,9 @@ private fun HapticVibrationCard( ) { Card( modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f)), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) ) { Row( @@ -455,7 +453,7 @@ private fun HapticVibrationCard( Column(modifier = Modifier.weight(1f)) { Text( - text = "Keypress Vibration (Haptic)", + text = "Getaran Tombol (Haptic)", style = MaterialTheme.typography.titleSmall.copy( fontWeight = FontWeight.Bold, fontSize = 14.sp @@ -464,7 +462,7 @@ private fun HapticVibrationCard( ) Spacer(modifier = Modifier.height(2.dp)) Text( - text = "Subtle tactile pulse on key touch", + text = "Getaran tactile halus saat tombol disentuh", style = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp), color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -491,11 +489,11 @@ private fun LiveSoundPlaygroundCard( ) { Card( modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(18.dp), + shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f) + containerColor = MaterialTheme.colorScheme.surface ), - border = BorderStroke(1.dp, FrogoPrimary.copy(alpha = 0.3f)), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) ) { Column( @@ -509,27 +507,26 @@ private fun LiveSoundPlaygroundCard( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "LIVE MECHANICAL KEYBOARD PLAYGROUND", - style = MaterialTheme.typography.labelSmall.copy( + text = "Playground Suara Mekanik", + style = MaterialTheme.typography.titleSmall.copy( fontWeight = FontWeight.Bold, - fontSize = 11.sp, - letterSpacing = 0.8.sp + fontSize = 14.sp ), color = FrogoPrimary ) Surface( shape = RoundedCornerShape(6.dp), - color = FrogoPrimary.copy(alpha = 0.15f) + color = FrogoPrimary.copy(alpha = 0.12f) ) { Text( - text = "TEST ACOUSTICS", + text = "Uji Suara", style = MaterialTheme.typography.labelSmall.copy( fontWeight = FontWeight.Bold, - fontSize = 9.sp + fontSize = 10.sp ), color = FrogoPrimary, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp) ) } } @@ -537,7 +534,7 @@ private fun LiveSoundPlaygroundCard( Spacer(modifier = Modifier.height(6.dp)) Text( - text = "Tap keycaps below to experience ${selectedSoundType.title} acoustics and tactile vibration:", + text = "Sentuh tombol di bawah untuk mendengarkan karakter suara ${selectedSoundType.title} dan getaran haptic:", style = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp), color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -559,7 +556,7 @@ private fun LiveSoundPlaygroundCard( } PlaygroundKeycap( - label = "SPACE", + label = "SPASI", onClick = onKeyTap, modifier = Modifier.weight(2f) ) @@ -585,15 +582,15 @@ private fun PlaygroundKeycap( Surface( modifier = modifier .height(44.dp) - .clip(RoundedCornerShape(10.dp)) + .clip(RoundedCornerShape(8.dp)) .clickable(onClick = onClick), - shape = RoundedCornerShape(10.dp), - color = if (isAction) FrogoPrimary else MaterialTheme.colorScheme.surface, + shape = RoundedCornerShape(8.dp), + color = if (isAction) FrogoPrimary else MaterialTheme.colorScheme.surfaceVariant, border = BorderStroke( 1.dp, - if (isAction) FrogoPrimary else MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f) + if (isAction) FrogoPrimary else MaterialTheme.colorScheme.outlineVariant ), - shadowElevation = 2.dp + shadowElevation = 0.dp ) { Box( modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestScreen.kt index 8226237..2a643b6 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestScreen.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestScreen.kt @@ -131,7 +131,7 @@ fun TestScreen( modifier = modifier.fillMaxSize(), topBar = { FrogoTopAppBar( - title = "Test Area", + title = "Playground Uji Coba", onBackClick = onBackClick ) } @@ -169,7 +169,7 @@ fun TestScreen( Tab( selected = activeTab == 1, onClick = { onTabSelected(1) }, - text = { Text("Inputs", fontWeight = FontWeight.SemiBold) }, + text = { Text("Input Khusus", fontWeight = FontWeight.SemiBold) }, icon = { Icon(Icons.Default.Keyboard, contentDescription = null, modifier = Modifier.size(20.dp)) } ) Tab( @@ -192,7 +192,7 @@ fun TestScreen( if (sandboxText.isNotEmpty()) { val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager clipboard.setPrimaryClip(ClipData.newPlainText("Sandbox Text", sandboxText)) - Toast.makeText(context, "Text copied to clipboard", Toast.LENGTH_SHORT).show() + Toast.makeText(context, "Teks disalin ke clipboard", Toast.LENGTH_SHORT).show() } } ) @@ -217,7 +217,7 @@ fun TestScreen( snippets = autoTextList, onSnippetClicked = { snippet -> onInsertText(snippet.body) - Toast.makeText(context, "Inserted '${snippet.title}' to sandbox", Toast.LENGTH_SHORT).show() + Toast.makeText(context, "Disisipkan '${snippet.title}' ke sandbox", Toast.LENGTH_SHORT).show() onTabSelected(0) } ) @@ -240,21 +240,22 @@ private fun KeyboardStatusCard( modifier: Modifier = Modifier ) { val statusColor = if (isFrogoActive) FrogoStatusSuccess else FrogoStatusWarning - val statusTitle = if (isFrogoActive) "Frogo Keyboard Active" else "Other Keyboard Active" + val statusTitle = if (isFrogoActive) "Frogo Keyboard Aktif" else "Keyboard Lain Aktif" val statusDesc = if (isFrogoActive) { - "Frogo Keyboard is default. Testing real keyboard layouts." + "Frogo Keyboard aktif sebagai input default. Anda sedang menguji tata letak asli." } else { - "External keyboard is active. Switch to test Frogo features." + "Metode input lain sedang aktif. Ganti untuk menguji fitur Frogo Keyboard." } val icon = if (isFrogoActive) Icons.Default.CheckCircle else Icons.Default.Warning Card( modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors( - containerColor = statusColor.copy(alpha = 0.08f) + containerColor = MaterialTheme.colorScheme.surface ), - border = BorderStroke(1.dp, statusColor.copy(alpha = 0.25f)) + border = BorderStroke(1.dp, statusColor.copy(alpha = 0.4f)), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) ) { Column( modifier = Modifier @@ -270,7 +271,7 @@ private fun KeyboardStatusCard( modifier = Modifier .size(38.dp) .clip(CircleShape) - .background(statusColor.copy(alpha = 0.16f)), + .background(statusColor.copy(alpha = 0.12f)), contentAlignment = Alignment.Center ) { Icon( @@ -307,21 +308,21 @@ private fun KeyboardStatusCard( Button( onClick = onChangeKeyboard, modifier = Modifier.weight(1f), - shape = RoundedCornerShape(10.dp), + shape = RoundedCornerShape(8.dp), colors = ButtonDefaults.buttonColors(containerColor = FrogoPrimary) ) { Icon(Icons.Default.Keyboard, contentDescription = null, modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.width(6.dp)) - Text("Switch Keyboard", fontSize = 13.sp) + Text("Ganti Keyboard", fontSize = 13.sp) } OutlinedButton( onClick = onGoToSettings, - shape = RoundedCornerShape(10.dp) + shape = RoundedCornerShape(8.dp) ) { Icon(Icons.Default.Settings, contentDescription = null, modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.width(4.dp)) - Text("Settings", fontSize = 13.sp) + Text("Pengaturan", fontSize = 13.sp) } } } @@ -336,13 +337,14 @@ private fun LiveMetricsCard( metrics: TypingMetrics, modifier: Modifier = Modifier ) { - ElevatedCard( + Card( modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.elevatedCardColors( + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp) + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) ) { Column( modifier = Modifier @@ -467,11 +469,12 @@ private fun SandboxTabContent( onValueChange = onTextChanged, modifier = Modifier .fillMaxWidth() - .height(200.dp), - placeholder = { Text("Type anywhere here with your keyboard to test metrics, latency, and keys...") }, - shape = RoundedCornerShape(16.dp), + .height(180.dp), + placeholder = { Text("Ketik di area ini dengan keyboard Anda untuk menguji metrik pengetikan, kecepatan, dan responsivitas tombol...") }, + shape = RoundedCornerShape(8.dp), colors = OutlinedTextFieldDefaults.colors( focusedBorderColor = FrogoPrimary, + unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant, focusedContainerColor = MaterialTheme.colorScheme.surface, unfocusedContainerColor = MaterialTheme.colorScheme.surface ) @@ -485,27 +488,27 @@ private fun SandboxTabContent( OutlinedButton( onClick = onCopyText, modifier = Modifier.weight(1f), - shape = RoundedCornerShape(10.dp) + shape = RoundedCornerShape(8.dp) ) { Icon(Icons.Default.ContentCopy, contentDescription = null, modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.width(6.dp)) - Text("Copy", fontSize = 13.sp) + Text("Salin", fontSize = 13.sp) } OutlinedButton( onClick = onResetTimer, modifier = Modifier.weight(1f), - shape = RoundedCornerShape(10.dp) + shape = RoundedCornerShape(8.dp) ) { Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.width(6.dp)) - Text("Reset WPM", fontSize = 13.sp) + Text("Reset", fontSize = 13.sp) } Button( onClick = onClearText, modifier = Modifier.weight(1f), - shape = RoundedCornerShape(10.dp), + shape = RoundedCornerShape(8.dp), colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.errorContainer, contentColor = MaterialTheme.colorScheme.onErrorContainer @@ -513,7 +516,7 @@ private fun SandboxTabContent( ) { Icon(Icons.Default.Delete, contentDescription = null, modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.width(6.dp)) - Text("Clear", fontSize = 13.sp) + Text("Hapus", fontSize = 13.sp) } } } @@ -562,7 +565,7 @@ private fun SpecializedInputsTabContent( } }, singleLine = true, - shape = RoundedCornerShape(14.dp), + shape = RoundedCornerShape(8.dp), keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Text, imeAction = ImeAction.Search @@ -574,10 +577,10 @@ private fun SpecializedInputsTabContent( value = numberInput, onValueChange = onNumberChange, modifier = Modifier.fillMaxWidth(), - label = { Text("Numeric Keypad (KeyboardType.Number)") }, + label = { Text("Input Angka (KeyboardType.Number)") }, placeholder = { Text("12345678") }, singleLine = true, - shape = RoundedCornerShape(14.dp), + shape = RoundedCornerShape(8.dp), keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Number, imeAction = ImeAction.Next @@ -589,10 +592,10 @@ private fun SpecializedInputsTabContent( value = phoneInput, onValueChange = onPhoneChange, modifier = Modifier.fillMaxWidth(), - label = { Text("Phone Keypad (KeyboardType.Phone)") }, + label = { Text("Input Telepon (KeyboardType.Phone)") }, placeholder = { Text("+62 812-3456-7890") }, singleLine = true, - shape = RoundedCornerShape(14.dp), + shape = RoundedCornerShape(8.dp), keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Phone, imeAction = ImeAction.Next @@ -604,10 +607,10 @@ private fun SpecializedInputsTabContent( value = passwordInput, onValueChange = onPasswordChange, modifier = Modifier.fillMaxWidth(), - label = { Text("Password (Visibility Toggle)") }, - placeholder = { Text("Enter secret password...") }, + label = { Text("Password (Toggle Visibilitas)") }, + placeholder = { Text("Masukkan kata sandi...") }, singleLine = true, - shape = RoundedCornerShape(14.dp), + shape = RoundedCornerShape(8.dp), visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Password, @@ -616,7 +619,7 @@ private fun SpecializedInputsTabContent( trailingIcon = { val icon = if (passwordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff IconButton(onClick = onTogglePasswordVisibility) { - Icon(icon, contentDescription = if (passwordVisible) "Hide password" else "Show password") + Icon(icon, contentDescription = if (passwordVisible) "Sembunyikan password" else "Tampilkan password") } } ) @@ -636,10 +639,10 @@ private fun SpecializedInputsTabContent( modifier = Modifier .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true) .fillMaxWidth(), - label = { Text("Searchable Dropdown (Filter Chips)") }, - placeholder = { Text("Type city name...") }, + label = { Text("Dropdown Pencarian") }, + placeholder = { Text("Ketik nama kota...") }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = dropdownExpanded) }, - shape = RoundedCornerShape(14.dp), + shape = RoundedCornerShape(8.dp), colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors() ) @@ -678,7 +681,7 @@ private fun AutoTextTabContent( verticalArrangement = Arrangement.spacedBy(10.dp) ) { Text( - text = "Tap any AutoText snippet to insert it into the test sandbox:", + text = "Sentuh salah satu template teks untuk menyisipkannya ke sandbox:", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -687,12 +690,14 @@ private fun AutoTextTabContent( Card( modifier = Modifier .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) .clickable { onSnippetClicked(snippet) }, - shape = RoundedCornerShape(14.dp), + shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface ), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) ) { Row( modifier = Modifier diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/theme/ThemeScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/ThemeScreen.kt index 29a8e85..3dc2ad6 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/theme/ThemeScreen.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/ThemeScreen.kt @@ -75,6 +75,7 @@ import com.frogobox.appkeyboard.model.KeyboardThemeModel import com.frogobox.appkeyboard.model.ThemeType import com.frogobox.appkeyboard.ui.theme.compose.FrogoKeyboardTheme import com.frogobox.appkeyboard.ui.theme.compose.FrogoPrimary +import com.frogobox.appkeyboard.ui.theme.compose.FrogoStatusSuccess import com.frogobox.appkeyboard.ui.theme.compose.FrogoTopAppBar enum class ThemeCategory(val title: String) { @@ -260,10 +261,10 @@ fun ThemeScreen( previewedTheme = current }, colors = ButtonDefaults.buttonColors(containerColor = FrogoPrimary), - shape = RoundedCornerShape(10.dp) + shape = RoundedCornerShape(8.dp) ) { Text( - text = "Apply Now", + text = "Terapkan Sekarang", fontWeight = FontWeight.Bold ) } @@ -272,10 +273,10 @@ fun ThemeScreen( TextButton( onClick = { confirmApplyDialogTheme = null } ) { - Text("Cancel") + Text("Batal") } }, - shape = RoundedCornerShape(20.dp) + shape = RoundedCornerShape(16.dp) ) } } @@ -312,14 +313,14 @@ private fun ThemePreviewHero( ) { Card( modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(20.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 3.dp), + shape = RoundedCornerShape(12.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface ), border = BorderStroke( - width = if (isActive) 2.dp else 1.dp, - color = if (isActive) FrogoPrimary else MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f) + width = if (isActive) 1.5.dp else 1.dp, + color = if (isActive) FrogoPrimary else MaterialTheme.colorScheme.outlineVariant ) ) { Column( @@ -387,9 +388,9 @@ private fun ThemePreviewHero( if (isActive) { Surface( - shape = RoundedCornerShape(12.dp), - color = Color(0xFF41AF60).copy(alpha = 0.12f), - border = BorderStroke(1.dp, Color(0xFF41AF60).copy(alpha = 0.4f)) + shape = RoundedCornerShape(8.dp), + color = FrogoStatusSuccess.copy(alpha = 0.12f), + border = BorderStroke(1.dp, FrogoStatusSuccess.copy(alpha = 0.4f)) ) { Row( modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), @@ -397,16 +398,16 @@ private fun ThemePreviewHero( ) { Icon( imageVector = Icons.Default.CheckCircle, - contentDescription = "Active", - tint = Color(0xFF41AF60), + contentDescription = "Aktif", + tint = FrogoStatusSuccess, modifier = Modifier.size(16.dp) ) Spacer(modifier = Modifier.width(4.dp)) Text( - text = "Active", + text = "Aktif", style = MaterialTheme.typography.labelMedium.copy( fontWeight = FontWeight.Bold, - color = Color(0xFF41AF60), + color = FrogoStatusSuccess, fontSize = 12.sp ) ) @@ -416,12 +417,12 @@ private fun ThemePreviewHero( Button( onClick = onApplyClick, colors = ButtonDefaults.buttonColors(containerColor = FrogoPrimary), - shape = RoundedCornerShape(12.dp), + shape = RoundedCornerShape(8.dp), contentPadding = PaddingValues(horizontal = 14.dp, vertical = 6.dp), modifier = Modifier.height(34.dp) ) { Text( - text = "Apply", + text = "Terapkan", style = MaterialTheme.typography.labelMedium.copy( fontWeight = FontWeight.Bold, fontSize = 12.sp @@ -605,7 +606,7 @@ private fun ThemeCategoryFilterRow( selectedLabelColor = FrogoPrimary, selectedLeadingIconColor = FrogoPrimary ), - shape = RoundedCornerShape(12.dp) + shape = RoundedCornerShape(8.dp) ) } } @@ -627,7 +628,7 @@ private fun ThemeGridCard( targetValue = when { isActive -> FrogoPrimary isPreviewed -> FrogoPrimary.copy(alpha = 0.6f) - else -> MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f) + else -> MaterialTheme.colorScheme.outlineVariant }, animationSpec = tween(durationMillis = 200), label = "cardBorderColor" @@ -636,15 +637,15 @@ private fun ThemeGridCard( Card( modifier = modifier .fillMaxWidth() - .clip(RoundedCornerShape(16.dp)) + .clip(RoundedCornerShape(12.dp)) .clickable { onCardClick() }, - shape = RoundedCornerShape(16.dp), - elevation = CardDefaults.cardElevation(defaultElevation = if (isPreviewed || isActive) 3.dp else 1.dp), + shape = RoundedCornerShape(12.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface ), border = BorderStroke( - width = if (isActive || isPreviewed) 2.dp else 1.dp, + width = if (isActive || isPreviewed) 1.5.dp else 1.dp, color = borderColor ) ) { @@ -772,8 +773,8 @@ private fun ThemeGridCard( // Floating Active Badge (Top Right) if (isActive) { Surface( - shape = RoundedCornerShape(bottomStart = 10.dp), - color = Color(0xFF41AF60), + shape = RoundedCornerShape(bottomStart = 8.dp), + color = FrogoStatusSuccess, modifier = Modifier.align(Alignment.TopEnd) ) { Row( @@ -782,13 +783,13 @@ private fun ThemeGridCard( ) { Icon( imageVector = Icons.Default.Check, - contentDescription = "Active", + contentDescription = "Aktif", tint = Color.White, modifier = Modifier.size(12.dp) ) Spacer(modifier = Modifier.width(3.dp)) Text( - text = "Active", + text = "Aktif", style = MaterialTheme.typography.labelSmall.copy( fontWeight = FontWeight.Bold, fontSize = 10.sp, @@ -799,12 +800,12 @@ private fun ThemeGridCard( } } else if (isPreviewed) { Surface( - shape = RoundedCornerShape(bottomStart = 10.dp), + shape = RoundedCornerShape(bottomStart = 8.dp), color = FrogoPrimary, modifier = Modifier.align(Alignment.TopEnd) ) { Text( - text = "Previewing", + text = "Preview", style = MaterialTheme.typography.labelSmall.copy( fontWeight = FontWeight.Bold, fontSize = 9.sp, diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/Color.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/Color.kt index c50fb02..ea01d43 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/Color.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/Color.kt @@ -2,26 +2,45 @@ package com.frogobox.appkeyboard.ui.theme.compose import androidx.compose.ui.graphics.Color -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) - -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) - -// Frogo Keyboard Specific Colors -val FrogoPrimary = Color(0xFF6200EE) -val FrogoPrimaryVariant = Color(0xFF3700B3) -val FrogoSecondary = Color(0xFF03DAC5) -val FrogoBackground = Color(0xFFFCFCFF) -val FrogoText = Color(0xFF000000) -val FrogoTextDark = Color(0xFFFFFFFF) -val FrogoGrey = Color(0xFF424242) -val FrogoDivider = Color(0xFFE0E0E0) -val FrogoBorder = Color(0xFFBDBDBD) - -// Status Colors -val FrogoStatusSuccess = Color(0xFF41AF60) -val FrogoStatusWarning = Color(0xFFF57C00) -val FrogoStatusFailed = Color(0xFFC53741) +// Tactile & Modern Tooling Color Palette (Anti-Slop Design System) +// Precision Steel Blue as Primary, Deep Slate for Dark Base, Clean Slate-White for Light Base + +// Primary Brand +val FrogoPrimary = Color(0xFF2563EB) // Precision Steel Blue (Light) +val FrogoPrimaryDark = Color(0xFF3B82F6) // Precision Steel Blue (Dark) +val FrogoPrimaryVariant = Color(0xFF1D4ED8) +val FrogoPrimaryContainer = Color(0xFFEFF6FF) +val FrogoPrimaryContainerDark = Color(0xFF1E3A8A) + +// Secondary & Accent +val FrogoSecondary = Color(0xFF0284C7) +val FrogoSecondaryDark = Color(0xFF38BDF8) + +// Backgrounds & Surfaces +val FrogoBackground = Color(0xFFF8FAFC) +val FrogoBackgroundDark = Color(0xFF0F172A) +val FrogoSurface = Color(0xFFFFFFFF) +val FrogoSurfaceDark = Color(0xFF1E293B) +val FrogoSurfaceVariantLight = Color(0xFFF1F5F9) +val FrogoSurfaceVariantDark = Color(0xFF334155) + +// Typography & Content +val FrogoText = Color(0xFF0F172A) +val FrogoTextDark = Color(0xFFF8FAFC) +val FrogoTextSecondary = Color(0xFF475569) +val FrogoTextSecondaryDark = Color(0xFF94A3B8) +val FrogoGrey = Color(0xFF64748B) + +// Borders & Tactile Separators +val FrogoDivider = Color(0xFFE2E8F0) +val FrogoDividerDark = Color(0xFF334155) +val FrogoBorder = Color(0xFFCBD5E1) +val FrogoBorderDark = Color(0xFF475569) + +// Semantic Status Colors (WCAG AA Compliant contrast ratio >= 4.5:1) +val FrogoStatusSuccess = Color(0xFF15803D) +val FrogoStatusSuccessDark = Color(0xFF22C55E) +val FrogoStatusWarning = Color(0xFFB45309) +val FrogoStatusWarningDark = Color(0xFFF59E0B) +val FrogoStatusFailed = Color(0xFFB91C1C) +val FrogoStatusFailedDark = Color(0xFFEF4444) diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/FrogoEmptyView.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/FrogoEmptyView.kt index 07150d5..58734c3 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/FrogoEmptyView.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/FrogoEmptyView.kt @@ -8,60 +8,86 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Inbox +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp /** - * Reusable Empty State Composable for lists across Frogo Keyboard. + * Reusable Empty State Composable for Frogo Keyboard app screens. + * Designed with Anti-Slop principles: human copy, purposeful icon, optional direct CTA. */ @Composable fun FrogoEmptyView( modifier: Modifier = Modifier, - title: String = "NO DATA FOUND", - subtitle: String = "Please insert data" + title: String = "Belum Ada Data Tersimpan", + subtitle: String = "Item yang Anda tambahkan akan tersimpan rapi dan dapat diakses kapan saja.", + icon: ImageVector = Icons.Default.Inbox, + actionButtonText: String? = null, + onActionClick: (() -> Unit)? = null ) { Column( modifier = modifier .fillMaxWidth() - .padding(32.dp), + .padding(horizontal = 24.dp, vertical = 40.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Icon( - imageVector = Icons.Default.Info, + imageVector = icon, contentDescription = null, - modifier = Modifier.size(72.dp), + modifier = Modifier.size(56.dp), tint = MaterialTheme.colorScheme.outline ) Spacer(modifier = Modifier.height(16.dp)) Text( - text = title.uppercase(), + text = title, style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.Bold, - fontSize = 18.sp + fontWeight = FontWeight.SemiBold, + fontSize = 17.sp ), color = MaterialTheme.colorScheme.onBackground, textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(6.dp)) Text( text = subtitle, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + lineHeight = 20.sp ) + + if (actionButtonText != null && onActionClick != null) { + Spacer(modifier = Modifier.height(18.dp)) + + Button( + onClick = onActionClick, + shape = MaterialTheme.shapes.small, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) + ) { + Text( + text = actionButtonText, + style = MaterialTheme.typography.labelLarge + ) + } + } } } diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/Theme.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/Theme.kt index 805fb5b..d6af6bf 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/Theme.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/Theme.kt @@ -8,29 +8,41 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80, - background = Color(0xFF1C1B1F), - surface = Color(0xFF1C1B1F), - onPrimary = Color(0xFF381E72), - onSecondary = Color(0xFF332D41), - onTertiary = Color(0xFF492532), - onBackground = Color(0xFFE6E1E5), - onSurface = Color(0xFFE6E1E5), + primary = FrogoPrimaryDark, + onPrimary = Color(0xFF0F172A), + primaryContainer = FrogoPrimaryContainerDark, + onPrimaryContainer = Color(0xFFDBEAFE), + secondary = FrogoSecondaryDark, + onSecondary = Color(0xFF0F172A), + tertiary = FrogoPrimaryDark, + onTertiary = Color(0xFF0F172A), + background = FrogoBackgroundDark, + onBackground = FrogoTextDark, + surface = FrogoSurfaceDark, + onSurface = FrogoTextDark, + surfaceVariant = FrogoSurfaceVariantDark, + onSurfaceVariant = FrogoTextSecondaryDark, + outline = FrogoBorderDark, + outlineVariant = FrogoDividerDark ) private val LightColorScheme = lightColorScheme( primary = FrogoPrimary, + onPrimary = Color.White, + primaryContainer = FrogoPrimaryContainer, + onPrimaryContainer = Color(0xFF1E3A8A), secondary = FrogoSecondary, + onSecondary = Color.White, tertiary = FrogoPrimaryVariant, - background = FrogoBackground, - surface = Color.White, - onPrimary = Color.White, - onSecondary = Color.Black, onTertiary = Color.White, + background = FrogoBackground, onBackground = FrogoText, + surface = FrogoSurface, onSurface = FrogoText, + surfaceVariant = FrogoSurfaceVariantLight, + onSurfaceVariant = FrogoTextSecondary, + outline = FrogoBorder, + outlineVariant = FrogoDivider ) @Composable diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/toggle/ToggleScreen.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/toggle/ToggleScreen.kt index f9de6e9..32e772a 100644 --- a/app/src/main/java/com/frogobox/appkeyboard/ui/toggle/ToggleScreen.kt +++ b/app/src/main/java/com/frogobox/appkeyboard/ui/toggle/ToggleScreen.kt @@ -242,7 +242,7 @@ private fun ToggleHeaderBanner( contentPadding = PaddingValues(horizontal = 8.dp, vertical = 2.dp) ) { Text( - text = "Enable All", + text = "Aktifkan Semua", style = MaterialTheme.typography.labelMedium.copy( fontWeight = FontWeight.SemiBold, fontSize = 12.sp @@ -258,7 +258,7 @@ private fun ToggleHeaderBanner( contentPadding = PaddingValues(horizontal = 8.dp, vertical = 2.dp) ) { Text( - text = "Disable All", + text = "Nonaktifkan Semua", style = MaterialTheme.typography.labelMedium.copy( fontWeight = FontWeight.Normal, fontSize = 12.sp @@ -288,19 +288,19 @@ private fun ToggleFilterRow( verticalAlignment = Alignment.CenterVertically ) { ToggleFilterChip( - text = "All ($totalCount)", + text = "Semua ($totalCount)", isSelected = selectedTab == ToggleFilterTab.ALL, onClick = { onTabSelected(ToggleFilterTab.ALL) } ) ToggleFilterChip( - text = "Active ($activeCount)", + text = "Aktif ($activeCount)", isSelected = selectedTab == ToggleFilterTab.ACTIVE, onClick = { onTabSelected(ToggleFilterTab.ACTIVE) } ) ToggleFilterChip( - text = "Disabled ($disabledCount)", + text = "Nonaktif ($disabledCount)", isSelected = selectedTab == ToggleFilterTab.DISABLED, onClick = { onTabSelected(ToggleFilterTab.DISABLED) } ) @@ -316,9 +316,9 @@ private fun ToggleFilterChip( ) { Surface( modifier = modifier - .clip(RoundedCornerShape(20.dp)) + .clip(RoundedCornerShape(8.dp)) .clickable(onClick = onClick), - shape = RoundedCornerShape(20.dp), + shape = RoundedCornerShape(8.dp), color = if (isSelected) { MaterialTheme.colorScheme.primary } else { @@ -329,7 +329,7 @@ private fun ToggleFilterChip( color = if (isSelected) { MaterialTheme.colorScheme.primary } else { - MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f) + MaterialTheme.colorScheme.outlineVariant } ) ) { @@ -344,7 +344,7 @@ private fun ToggleFilterChip( } else { MaterialTheme.colorScheme.onSurfaceVariant }, - modifier = Modifier.padding(horizontal = 14.dp, vertical = 6.dp) + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) ) } } @@ -363,21 +363,21 @@ private fun ToggleFeatureCard( Card( modifier = modifier .fillMaxWidth() - .clip(RoundedCornerShape(18.dp)) + .clip(RoundedCornerShape(12.dp)) .clickable { onCheckedChange(!isChecked) }, - shape = RoundedCornerShape(18.dp), + shape = RoundedCornerShape(12.dp), elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), border = BorderStroke( - width = 1.dp, + width = if (isChecked) 1.5.dp else 1.dp, color = if (isChecked) { - primaryColor.copy(alpha = 0.45f) + primaryColor.copy(alpha = 0.5f) } else { - outlineVariant.copy(alpha = 0.35f) + outlineVariant } ), colors = CardDefaults.cardColors( containerColor = if (isChecked) { - primaryColor.copy(alpha = 0.08f) + primaryColor.copy(alpha = 0.06f) } else { surfaceColor } diff --git a/app/src/test/java/com/frogobox/appkeyboard/KeyboardImeComposeUnitTest.kt b/app/src/test/java/com/frogobox/appkeyboard/KeyboardImeComposeUnitTest.kt index 53ba2f1..aa4d423 100644 --- a/app/src/test/java/com/frogobox/appkeyboard/KeyboardImeComposeUnitTest.kt +++ b/app/src/test/java/com/frogobox/appkeyboard/KeyboardImeComposeUnitTest.kt @@ -157,4 +157,25 @@ class KeyboardImeComposeUnitTest { assertTrue("Min width must be at least 72dp to avoid multi-line label clipping", featureItemMinWidthDp >= 72) } + @Test + fun testFeatureHeaderPositioningAndDistributionLogic() { + val standardScreenWidthDp = 360 + val minComfortableWidthDp = 70 + + // Case 1: 4 default items (Auto Text, Movie, Change Keyboard, Setting) + val fourItems = listOf("menu_auto_text", "menu_movie", "menu_change_keyboard", "menu_setting") + val canFitFourEvenly = fourItems.size <= 5 && (standardScreenWidthDp / fourItems.size) >= minComfortableWidthDp + assertTrue("4 items must be evenly distributed across full width", canFitFourEvenly) + val fourItemWidth = standardScreenWidthDp.toFloat() / fourItems.size + assertEquals(90.0f, fourItemWidth, 0.01f) + + // Case 2: Many items (e.g. 7 items) switch to scrollable with uniform slot width + val manyItems = KeyboardFeatureType.entries.map { it.id } + val canFitManyEvenly = manyItems.size <= 5 && (standardScreenWidthDp / manyItems.size) >= minComfortableWidthDp + assertFalse("Many items (>5) must use scrollable layout", canFitManyEvenly) + + val dynamicSlotWidth = (standardScreenWidthDp / 4.25f).coerceIn(72f, 92f) + assertTrue("Dynamic slot width must ensure at least 4 items visible with peeking 5th", dynamicSlotWidth in 72f..92f) + } + } diff --git a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboardScreen.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboardScreen.kt index e80bb12..2448c8f 100644 --- a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboardScreen.kt +++ b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiKeyboardScreen.kt @@ -1,5 +1,6 @@ package com.frogobox.libkeyboard.ui.emoji +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -251,14 +252,17 @@ fun EmojiCategoryBar( Surface( modifier = Modifier - .clip(RoundedCornerShape(16.dp)) + .clip(RoundedCornerShape(8.dp)) .clickable { onCategorySelected(type) }, - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(8.dp), color = if (isSelected) { MaterialTheme.colorScheme.primaryContainer } else { Color.Transparent - } + }, + border = if (isSelected) { + BorderStroke(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)) + } else null ) { Box( modifier = Modifier diff --git a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboardScreen.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboardScreen.kt index 0da41d1..e64d449 100644 --- a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboardScreen.kt +++ b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/MainKeyboardScreen.kt @@ -1,5 +1,6 @@ package com.frogobox.libkeyboard.ui.main +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable @@ -193,6 +194,7 @@ private fun KeyItemView( ), shape = RoundedCornerShape(6.dp), color = backgroundColor, + border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)), shadowElevation = if (isActionKey) 1.dp else 1.5.dp ) { Box( diff --git a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/theme/FrogoLibKeyboardTheme.kt b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/theme/FrogoLibKeyboardTheme.kt index 6e1d302..1163131 100644 --- a/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/theme/FrogoLibKeyboardTheme.kt +++ b/lib-keyboard/src/main/java/com/frogobox/libkeyboard/ui/theme/FrogoLibKeyboardTheme.kt @@ -11,47 +11,49 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -val KeyboardTealPrimary = Color(0xFF009688) -val KeyboardTealPrimaryDark = Color(0xFF80CBC4) -val KeyboardTealPrimaryContainer = Color(0xFFB2DFDB) -val KeyboardTealPrimaryContainerDark = Color(0xFF004D40) +val KeyboardTealPrimary = Color(0xFF2563EB) +val KeyboardTealPrimaryDark = Color(0xFF3B82F6) +val KeyboardTealPrimaryContainer = Color(0xFFDBEAFE) +val KeyboardTealPrimaryContainerDark = Color(0xFF1E3A8A) val KeyboardSurfaceLight = Color(0xFFFFFFFF) -val KeyboardSurfaceDark = Color(0xFF1E1E1E) -val KeyboardBackgroundLight = Color(0xFFF5F5F5) -val KeyboardBackgroundDark = Color(0xFF121212) +val KeyboardSurfaceDark = Color(0xFF0F172A) +val KeyboardBackgroundLight = Color(0xFFF8FAFC) +val KeyboardBackgroundDark = Color(0xFF020617) -val KeypadLight = Color(0xFFE3E5E8) -val KeypadDark = Color(0xFF2C2C2C) -val KeypadActionLight = Color(0xFFCFD4D9) -val KeypadActionDark = Color(0xFF3A3A3A) +val KeypadLight = Color(0xFFF1F5F9) +val KeypadDark = Color(0xFF1E293B) +val KeypadActionLight = Color(0xFFE2E8F0) +val KeypadActionDark = Color(0xFF334155) private val DarkColorScheme = darkColorScheme( primary = KeyboardTealPrimaryDark, - onPrimary = Color(0xFF003731), + onPrimary = Color(0xFF020617), primaryContainer = KeyboardTealPrimaryContainerDark, - onPrimaryContainer = Color(0xFFB2DFDB), + onPrimaryContainer = Color(0xFFDBEAFE), surface = KeyboardSurfaceDark, - onSurface = Color(0xFFE0E0E0), - surfaceVariant = Color(0xFF2A2A2A), - onSurfaceVariant = Color(0xFFB0B0B0), + onSurface = Color(0xFFF8FAFC), + surfaceVariant = Color(0xFF1E293B), + onSurfaceVariant = Color(0xFF94A3B8), background = KeyboardBackgroundDark, - onBackground = Color(0xFFE0E0E0), - outline = Color(0xFF383838) + onBackground = Color(0xFFF8FAFC), + outline = Color(0xFF334155), + outlineVariant = Color(0xFF1E293B) ) private val LightColorScheme = lightColorScheme( primary = KeyboardTealPrimary, onPrimary = Color.White, primaryContainer = KeyboardTealPrimaryContainer, - onPrimaryContainer = Color(0xFF004D40), + onPrimaryContainer = Color(0xFF1E3A8A), surface = KeyboardSurfaceLight, - onSurface = Color(0xFF212121), - surfaceVariant = Color(0xFFF0F0F0), - onSurfaceVariant = Color(0xFF616161), + onSurface = Color(0xFF0F172A), + surfaceVariant = Color(0xFFF1F5F9), + onSurfaceVariant = Color(0xFF64748B), background = KeyboardBackgroundLight, - onBackground = Color(0xFF212121), - outline = Color(0xFFE0E0E0) + onBackground = Color(0xFF0F172A), + outline = Color(0xFFCBD5E1), + outlineVariant = Color(0xFFE2E8F0) ) @Composable