diff --git a/.gitignore b/.gitignore
index 06b4d63..9365c95 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,14 +50,14 @@ app.*.map.json
/android/app/release
# Miscellaneous
-build application files
+# build application files
*.apk
*.ap_
*.aab
build/
-Local configuration file (sdk path, etc)
+# Local configuration file (sdk path, etc)
app/version.properties
# Flutter Modules
@@ -79,10 +79,13 @@ build
/captures
.externalNativeBuild
-generated files
+# generated files
bin/
gen/
.externalNativeBuild/
# SonarQube
.sonar/
+buildSrc/.kotlin
+.kotlin
+workflow
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 2e5c938..89e6f12 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,10 +1,8 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
-import java.text.SimpleDateFormat
-import java.util.Date
plugins {
alias(libs.plugins.android.application)
- alias(libs.plugins.jetbrains.kotlin.android)
+ alias(libs.plugins.jetbrains.kotlin.compose)
alias(libs.plugins.hilt)
alias(libs.plugins.ksp)
id("kotlin-parcelize")
@@ -15,15 +13,14 @@ ksp {
}
base {
- // Naming APK // AAB
- val timestamp = SimpleDateFormat("dd-MM-yyyy_hh-mm").format(Date())
- archivesName = "${ProjectSetting.NAME_APK}-${ProjectSetting.PROJECT_VERSION_CODE}-[${ProjectSetting.PROJECT_VERSION_NAME}]-$timestamp"
+ // Naming APK // AAB (stable name avoids invalidating incremental tasks every minute)
+ archivesName = "${ProjectSetting.NAME_APK}-${ProjectSetting.PROJECT_VERSION_CODE}-[${ProjectSetting.PROJECT_VERSION_NAME}]"
}
android {
namespace = ProjectSetting.PROJECT_NAME_SPACE_APP
- compileSdk = ProjectSetting.PROJECT_TARGET_SDK
+ compileSdk = ProjectSetting.PROJECT_COMPILE_SDK
defaultConfig {
applicationId = ProjectSetting.PROJECT_APP_ID
@@ -97,6 +94,8 @@ android {
buildFeatures {
buildConfig = true
viewBinding = true
+ compose = true
+ resValues = true
}
compileOptions {
@@ -115,14 +114,32 @@ 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.viewmodel.compose)
+
implementation(project(DependencyGradle.MODULE_LIB_FROGO_KEYBOARD))
implementation(libs.androidx.work.ktx)
implementation(libs.google.hilt)
+ // Room & Coroutines
+ 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)
+ testImplementation(libs.junit)
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 625d548..30cf068 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -14,7 +14,7 @@
android:label="@string/app_name"
android:roundIcon="@drawable/ic_frogobox"
android:supportsRtl="true"
- android:theme="@style/Theme.Research"
+ android:theme="@style/Theme.ResearchNoActionBar"
tools:targetApi="tiramisu">
+
+
diff --git a/app/src/main/assets/text/dictionary_en.txt b/app/src/main/assets/text/dictionary_en.txt
new file mode 100644
index 0000000..c97d0d0
--- /dev/null
+++ b/app/src/main/assets/text/dictionary_en.txt
@@ -0,0 +1,895 @@
+the
+be
+to
+of
+and
+a
+in
+that
+have
+i
+it
+for
+not
+on
+with
+he
+as
+you
+do
+at
+this
+but
+his
+by
+from
+they
+we
+say
+her
+she
+or
+an
+will
+my
+one
+all
+would
+there
+their
+what
+so
+up
+out
+if
+about
+who
+get
+which
+go
+me
+when
+make
+can
+like
+time
+no
+just
+him
+know
+take
+people
+into
+year
+your
+good
+some
+could
+them
+see
+other
+than
+then
+now
+look
+only
+come
+its
+over
+think
+also
+back
+after
+use
+two
+how
+our
+work
+first
+well
+way
+even
+new
+want
+because
+any
+these
+give
+day
+most
+us
+is
+are
+was
+were
+been
+has
+had
+did
+doing
+done
+having
+does
+said
+says
+saying
+went
+gone
+going
+goes
+got
+getting
+made
+making
+makes
+took
+taking
+takes
+knew
+knowing
+knows
+came
+coming
+comes
+looked
+looking
+looks
+thought
+thinking
+thinks
+saw
+seen
+seeing
+sees
+wanted
+wanting
+wants
+gave
+given
+giving
+gives
+found
+finding
+finds
+find
+tell
+told
+telling
+tells
+asked
+asking
+asks
+ask
+worked
+working
+works
+called
+calling
+calls
+call
+felt
+feeling
+feels
+feel
+tried
+trying
+tries
+try
+left
+leaving
+leaves
+leave
+helped
+helping
+helps
+help
+talked
+talking
+talks
+talk
+turned
+turning
+turns
+turn
+started
+starting
+starts
+start
+showed
+showing
+shows
+show
+heard
+hearing
+hears
+hear
+played
+playing
+plays
+play
+ran
+running
+runs
+run
+moved
+moving
+moves
+move
+liked
+liking
+likes
+lived
+living
+lives
+live
+believed
+believing
+believes
+believe
+held
+holding
+holds
+hold
+brought
+bringing
+brings
+bring
+happened
+happening
+happens
+happen
+wrote
+written
+writing
+writes
+write
+provided
+providing
+provides
+provide
+sat
+sitting
+sits
+sit
+stood
+standing
+stands
+stand
+lost
+losing
+loses
+lose
+paid
+paying
+pays
+pay
+met
+meeting
+meets
+meet
+included
+including
+includes
+include
+continued
+continuing
+continues
+continue
+set
+setting
+sets
+learned
+learning
+learns
+learn
+changed
+changing
+changes
+change
+led
+leading
+leads
+lead
+understood
+understanding
+understands
+understand
+watched
+watching
+watches
+watch
+followed
+following
+follows
+follow
+stopped
+stopping
+stops
+stop
+created
+creating
+creates
+create
+spoke
+spoken
+speaking
+speaks
+speak
+read
+reading
+reads
+allowed
+allowing
+allows
+allow
+added
+adding
+adds
+add
+spent
+spending
+spends
+spend
+grew
+growing
+grows
+grow
+opened
+opening
+opens
+open
+walked
+walking
+walks
+walk
+won
+winning
+wins
+win
+offered
+offering
+offers
+offer
+remembered
+remembering
+remembers
+remember
+loved
+loving
+loves
+love
+considered
+considering
+considers
+consider
+appeared
+appearing
+appears
+appear
+bought
+buying
+buys
+buy
+waited
+waiting
+waits
+wait
+served
+serving
+serves
+serve
+died
+dying
+dies
+die
+sent
+sending
+sends
+send
+expected
+expecting
+expects
+expect
+built
+building
+builds
+build
+stayed
+staying
+stays
+stay
+fell
+fallen
+falling
+falls
+fall
+cut
+cutting
+cuts
+reached
+reaching
+reaches
+reach
+killed
+killing
+kills
+kill
+remained
+remaining
+remains
+remain
+suggest
+suggested
+suggesting
+suggests
+suggestion
+suggestions
+predict
+predicted
+predicting
+predicts
+prediction
+predictions
+correct
+corrected
+correcting
+corrects
+correction
+autocorrect
+keyboard
+keyboards
+keypad
+typing
+type
+typed
+types
+android
+google
+kotlin
+compose
+developer
+development
+device
+screen
+layout
+interface
+system
+mobile
+application
+software
+hardware
+computer
+phone
+message
+messages
+text
+texts
+notification
+settings
+toggle
+feature
+features
+theme
+color
+background
+button
+input
+connection
+word
+words
+string
+char
+character
+dictionary
+search
+query
+result
+results
+candidate
+candidates
+hello
+hi
+hey
+welcome
+thanks
+thank
+please
+sorry
+excuse
+morning
+afternoon
+evening
+night
+today
+tomorrow
+yesterday
+weekend
+monday
+tuesday
+wednesday
+thursday
+friday
+saturday
+sunday
+january
+february
+march
+april
+may
+june
+july
+august
+september
+october
+november
+december
+home
+office
+school
+university
+college
+student
+teacher
+friend
+friends
+family
+brother
+sister
+father
+mother
+parent
+child
+children
+person
+people
+group
+team
+company
+business
+market
+product
+service
+money
+price
+order
+cart
+customer
+client
+support
+account
+user
+password
+login
+signup
+register
+profile
+setting
+privacy
+security
+policy
+terms
+about
+contact
+info
+information
+help
+faq
+feedback
+review
+rating
+star
+stars
+share
+download
+install
+update
+version
+release
+build
+test
+testing
+tested
+tester
+bug
+issue
+fix
+fixed
+fixing
+error
+warning
+exception
+crash
+failed
+passed
+success
+successful
+status
+check
+active
+enable
+disabled
+enabled
+disable
+open
+close
+closed
+cancel
+confirm
+accept
+delete
+remove
+edit
+editor
+save
+saved
+saving
+copy
+paste
+cut
+select
+all
+none
+clear
+done
+next
+previous
+back
+forward
+up
+down
+left
+right
+center
+middle
+top
+bottom
+start
+end
+first
+last
+min
+max
+small
+medium
+large
+huge
+fast
+slow
+easy
+hard
+simple
+complex
+clean
+clear
+true
+false
+yes
+no
+ok
+okay
+fine
+great
+awesome
+cool
+nice
+super
+wonderful
+amazing
+excellent
+perfect
+best
+better
+worse
+worst
+high
+higher
+highest
+low
+lower
+lowest
+long
+longer
+longest
+short
+shorter
+shortest
+big
+bigger
+biggest
+little
+few
+many
+much
+more
+less
+most
+least
+both
+either
+neither
+each
+every
+another
+such
+same
+different
+important
+real
+possible
+able
+available
+public
+private
+personal
+general
+special
+main
+major
+common
+current
+recent
+popular
+hot
+new
+old
+young
+free
+busy
+ready
+sure
+certain
+safe
+secure
+strong
+weak
+rich
+poor
+happy
+glad
+sad
+angry
+excited
+bored
+tired
+hungry
+fresh
+warm
+hot
+cold
+cool
+bright
+dark
+light
+heavy
+blue
+green
+red
+yellow
+orange
+purple
+pink
+black
+white
+gray
+brown
+gold
+silver
+apple
+orange
+banana
+fruit
+coffee
+tea
+water
+food
+dinner
+lunch
+breakfast
+restaurant
+store
+shop
+game
+movie
+music
+song
+video
+photo
+picture
+image
+camera
+audio
+sound
+news
+web
+website
+internet
+network
+online
+offline
+link
+page
+post
+comment
+like
+follow
+view
+play
+pause
+stop
+record
+player
+podcast
+book
+read
+write
+note
+notes
+document
+file
+folder
+data
+database
+table
+list
+grid
+item
+items
+header
+footer
+dialog
+sheet
+tab
+tabs
+box
+card
+panel
+window
+view
+viewmodel
+activity
+fragment
+adapter
+service
+receiver
+provider
+broadcast
+intent
+permission
+location
+map
+navigation
+route
+distance
+speed
+time
+hour
+minute
+second
+clock
+alarm
+calendar
+event
+date
+weather
+cloud
+rain
+sun
+wind
+snow
+travel
+trip
+flight
+hotel
+car
+bus
+train
+plane
+ticket
+city
+country
+place
+world
+earth
+space
+star
+moon
diff --git a/app/src/main/java/com/frogobox/appkeyboard/MainApp.kt b/app/src/main/java/com/frogobox/appkeyboard/MainApp.kt
index cc39066..68cfb5e 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/MainApp.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/MainApp.kt
@@ -1,12 +1,13 @@
package com.frogobox.appkeyboard
import android.content.Context
-import android.os.Build
+import androidx.core.os.ConfigurationCompat
import androidx.emoji2.bundled.BundledEmojiCompatConfig
import androidx.emoji2.text.EmojiCompat
import com.frogobox.sdk.FrogoApplication
import dagger.hilt.android.HiltAndroidApp
import java.util.Locale
+import java.util.concurrent.Executors
/**
* Created by Faisal Amir on 24/10/22
@@ -28,11 +29,7 @@ class MainApp : FrogoApplication() {
fun getContext(): Context = instance.applicationContext
fun getCurrentLocale(): Locale? {
- return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- instance.resources.configuration.locales[0]
- } else {
- instance.resources.configuration.locale
- }
+ return ConfigurationCompat.getLocales(instance.resources.configuration)[0]
}
}
@@ -44,7 +41,7 @@ class MainApp : FrogoApplication() {
}
private fun setupEmojiCompat() {
- val config = BundledEmojiCompatConfig(this)
+ val config = BundledEmojiCompatConfig(this, Executors.newSingleThreadExecutor())
EmojiCompat.init(config)
}
diff --git a/app/src/main/java/com/frogobox/appkeyboard/common/base/BaseComposeActivity.kt b/app/src/main/java/com/frogobox/appkeyboard/common/base/BaseComposeActivity.kt
new file mode 100644
index 0000000..f93185e
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/common/base/BaseComposeActivity.kt
@@ -0,0 +1,31 @@
+package com.frogobox.appkeyboard.common.base
+
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.compose.runtime.Composable
+import com.frogobox.appkeyboard.BuildConfig
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoKeyboardTheme
+import com.frogobox.sdk.view.FrogoActivity
+
+/**
+ * BaseComposeActivity provides a clean, unified base for Jetpack Compose screens
+ * while maintaining compatibility with Frogo SDK's toolbar & debug setup.
+ */
+abstract class BaseComposeActivity : FrogoActivity() {
+
+ override fun setupDebugMode(): Boolean {
+ return BuildConfig.DEBUG
+ }
+
+ override fun onCreateExt(savedInstanceState: Bundle?) {
+ super.onCreateExt(savedInstanceState)
+ setContent {
+ FrogoKeyboardTheme {
+ Content()
+ }
+ }
+ }
+
+ @Composable
+ abstract fun Content()
+}
diff --git a/app/src/main/java/com/frogobox/appkeyboard/common/base/BaseComposeMainActivity.kt b/app/src/main/java/com/frogobox/appkeyboard/common/base/BaseComposeMainActivity.kt
new file mode 100644
index 0000000..40cafca
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/common/base/BaseComposeMainActivity.kt
@@ -0,0 +1,31 @@
+package com.frogobox.appkeyboard.common.base
+
+import com.frogobox.sdk.delegate.piracy.PiracyCallback
+import com.frogobox.sdk.delegate.piracy.PiracyDelegates
+import com.frogobox.sdk.delegate.piracy.PiracyDelegatesImpl
+import com.frogobox.sdk.delegate.piracy.PiracyMessage
+import com.frogobox.sdk.ext.openPlayStore
+
+/**
+ * BaseComposeMainActivity provides Compose baseline combined with Frogo SDK PiracyDelegates.
+ */
+abstract class BaseComposeMainActivity : BaseComposeActivity(),
+ PiracyDelegates by PiracyDelegatesImpl() {
+
+ override fun setupDelegates() {
+ super.setupDelegates()
+ setupPiracyDelegate(this, this)
+ }
+
+ override fun setupPiracyMode() {
+ connectPiracyChecker(object : PiracyCallback {
+ override fun doOnPirated(message: PiracyMessage) {
+ showPiracedDialog(message, object : PiracyCallback {
+ override fun doOnPirated(message: PiracyMessage) {
+ openPlayStore(packageName)
+ }
+ })
+ }
+ })
+ }
+}
diff --git a/app/src/main/java/com/frogobox/appkeyboard/common/ext/DateExt.kt b/app/src/main/java/com/frogobox/appkeyboard/common/ext/DateExt.kt
index 7fad923..c4521a7 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/common/ext/DateExt.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/common/ext/DateExt.kt
@@ -66,9 +66,10 @@ const val DAY_WITH_DATE_TIME_LOCALE_FULL = "EEEE, dd MMMM yyyy HH:mm" // Senin,
if (date != null) {
try {
- timestamp = df.parse(date).time / 1000
+ timestamp = df.parse(date)?.time?.div(1000) ?: (Date().time / 1000)
} catch (e: ParseException) {
e.printStackTrace()
+ timestamp = Date().time / 1000
}
} else {
@@ -98,25 +99,39 @@ const val DAY_WITH_DATE_TIME_LOCALE_FULL = "EEEE, dd MMMM yyyy HH:mm" // Senin,
}
fun convertDateNewFormat(string: String?): String {
- val formatter = SimpleDateFormat(DATE_ENGLISH_YYYY_MM_DD, Locale.getDefault())
- val date = formatter.parse(string) as Date
- val newFormat = SimpleDateFormat(DATE_DD_MM_YYYY, Locale("EN"))
- return newFormat.format(date)
+ if (string == null) return ""
+ return try {
+ val formatter = SimpleDateFormat(DATE_ENGLISH_YYYY_MM_DD, Locale.getDefault())
+ val date = formatter.parse(string) ?: return ""
+ val newFormat = SimpleDateFormat(DATE_DD_MM_YYYY, Locale.ENGLISH)
+ newFormat.format(date)
+ } catch (e: Exception) {
+ ""
+ }
}
fun convertLongDateNewFormat(string: String?): String {
- val formatter = SimpleDateFormat(DATE_TIME_STANDARD, Locale.getDefault())
- val date = formatter.parse(string) as Date
- val newFormat = SimpleDateFormat("dd-MM-yy HH:mm:ss", Locale("EN"))
- return newFormat.format(date)
+ if (string == null) return ""
+ return try {
+ val formatter = SimpleDateFormat(DATE_TIME_STANDARD, Locale.getDefault())
+ val date = formatter.parse(string) ?: return ""
+ val newFormat = SimpleDateFormat("dd-MM-yy HH:mm:ss", Locale.ENGLISH)
+ newFormat.format(date)
+ } catch (e: Exception) {
+ ""
+ }
}
fun revertFromLongDateNewFormat(string: String?): String {
- val formatter = SimpleDateFormat("dd-MM-yy HH:mm:ss", Locale("EN"))
- val date = formatter.parse(string) as Date
- val newFormat = SimpleDateFormat(DATE_TIME_STANDARD, Locale.getDefault())
- val finalString = newFormat.format(date)
- return finalString
+ if (string == null) return ""
+ return try {
+ val formatter = SimpleDateFormat("dd-MM-yy HH:mm:ss", Locale.ENGLISH)
+ val date = formatter.parse(string) ?: return ""
+ val newFormat = SimpleDateFormat(DATE_TIME_STANDARD, Locale.getDefault())
+ newFormat.format(date)
+ } catch (e: Exception) {
+ ""
+ }
}
fun convertTargetDate(string: String?): String {
diff --git a/app/src/main/java/com/frogobox/appkeyboard/data/local/autotext/AutoTextDao.kt b/app/src/main/java/com/frogobox/appkeyboard/data/local/autotext/AutoTextDao.kt
index 1fa19d7..af93df6 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/data/local/autotext/AutoTextDao.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/data/local/autotext/AutoTextDao.kt
@@ -8,50 +8,50 @@ package com.frogobox.appkeyboard.data.local.autotext
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
+import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.frogobox.appkeyboard.model.AutoTextEntity
import com.frogobox.appkeyboard.model.AutoTextLabelType
-import io.reactivex.rxjava3.core.Completable
-import io.reactivex.rxjava3.core.Single
+import kotlinx.coroutines.flow.Flow
@Dao
interface AutoTextDao {
- @Insert
- fun insert(autoText: AutoTextEntity) : Completable
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insert(autoText: AutoTextEntity)
- @Insert
- fun insert(autoTexts: List) : Completable
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insert(autoTexts: List)
@Update
- fun update(autoText: AutoTextEntity) : Completable
+ suspend fun update(autoText: AutoTextEntity)
@Delete
- fun delete(autoText: AutoTextEntity) : Completable
+ suspend fun delete(autoText: AutoTextEntity)
- @Query("DELETE FROM auto_text WHERE id in (:idList)")
- fun delete(idList: List): Completable
+ @Query("DELETE FROM auto_text WHERE id IN (:idList)")
+ suspend fun delete(idList: List)
@Query("DELETE FROM auto_text")
- fun nukeData(): Completable
+ suspend fun nukeData()
@Query("SELECT * FROM auto_text ORDER BY id ASC")
- fun getAll() : Single>
+ fun getAll(): Flow>
@Query("SELECT * FROM auto_text WHERE id = :id")
- fun getById(id: Int) : Single>
+ fun getById(id: Int): Flow
@Query("SELECT * FROM auto_text WHERE title LIKE '%' || :search || '%' ORDER BY id ASC")
- fun getByTitle(search: String): Single>
+ fun getByTitle(search: String): Flow>
- @Query("SELECT * FROM auto_text WHERE label = :search ORDER BY id ASC")
- fun getByLabel(search: AutoTextLabelType) : Single>
+ @Query("SELECT * FROM auto_text WHERE label = :label ORDER BY id ASC")
+ fun getByLabel(label: AutoTextLabelType): Flow>
@Query("SELECT * FROM auto_text WHERE body LIKE '%' || :search || '%' ORDER BY id ASC")
- fun getByBody(search: String) : Single>
+ fun getByBody(search: String): Flow>
@Query("SELECT * FROM auto_text WHERE (title LIKE '%' || :search || '%' OR body LIKE '%' || :search || '%') ORDER BY id ASC")
- fun getByTitleOrBody(search: String): Single>
+ fun getByTitleOrBody(search: String): Flow>
}
\ No newline at end of file
diff --git a/app/src/main/java/com/frogobox/appkeyboard/data/local/db/AppDatabase.kt b/app/src/main/java/com/frogobox/appkeyboard/data/local/db/AppDatabase.kt
index 89ff9c9..2b8e0c5 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/data/local/db/AppDatabase.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/data/local/db/AppDatabase.kt
@@ -40,7 +40,7 @@ abstract class AppDatabase : RoomDatabase() {
private fun buildDatabase(context: Context): AppDatabase {
return if (BuildConfig.DEBUG) {
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, DATABASE_NAME)
- .fallbackToDestructiveMigration() // FOR DEVELOPMENT ONLY !!!!
+ .fallbackToDestructiveMigration(dropAllTables = true) // FOR DEVELOPMENT ONLY !!!!
.build()
} else {
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, DATABASE_NAME)
diff --git a/app/src/main/java/com/frogobox/appkeyboard/data/remote/ApiService.kt b/app/src/main/java/com/frogobox/appkeyboard/data/remote/ApiService.kt
index 54b8348..5f92e83 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/data/remote/ApiService.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/data/remote/ApiService.kt
@@ -7,7 +7,6 @@ import com.chuckerteam.chucker.api.RetentionManager
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
-import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
@@ -54,7 +53,6 @@ object ApiService {
return Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
- .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.client(client)
.build()
.create(T::class.java)
diff --git a/app/src/main/java/com/frogobox/appkeyboard/model/KeyboardFeatureType.kt b/app/src/main/java/com/frogobox/appkeyboard/model/KeyboardFeatureType.kt
index a727add..0cba63f 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/model/KeyboardFeatureType.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/model/KeyboardFeatureType.kt
@@ -20,6 +20,7 @@ enum class KeyboardFeatureType(val id: String, val text: String, val icon: Int)
TEMPLATE_TEXT_SALE("menu_template_text_sale", "Sale Admin", R.drawable.ic_menu_ps_sale),
TEMPLATE_TEXT_LOVE("menu_template_text_love", "Love Emoji", R.drawable.ic_menu_ps_love),
TEMPLATE_TEXT_GREETING("menu_template_text_greeting", "Greeting", R.drawable.ic_menu_ps_greeting),
+ SUGGESTION("menu_suggestion", "Word Suggestion", R.drawable.ic_menu_suggestion),
CHANGE_KEYBOARD("menu_change_keyboard", "Change Keyboard", R.drawable.ic_menu_keyboard),
SETTING("menu_setting", "Setting", R.drawable.ic_menu_setting);
diff --git a/app/src/main/java/com/frogobox/appkeyboard/model/KeyboardThemeType.kt b/app/src/main/java/com/frogobox/appkeyboard/model/KeyboardThemeType.kt
index c65373c..8143679 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/model/KeyboardThemeType.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/model/KeyboardThemeType.kt
@@ -21,12 +21,17 @@ enum class KeyboardThemeType(
private val themeType: ThemeType,
private val background: Int,
) {
- DEFAULT("Default", "Default Color", ThemeType.COLOR, R.color.color_bg_keyboard_default),
- RED("Red", "Red Color", ThemeType.COLOR, R.color.color_bg_keyboard_red),
- GREEN("Green", "Green Color", ThemeType.COLOR, R.color.color_bg_keyboard_green),
- YELLOW("Yellow", "Yellow Color", ThemeType.COLOR, R.color.color_bg_keyboard_yellow),
- BLUE("Blue", "Blue Color", ThemeType.COLOR, R.color.color_bg_keyboard_blue),
- IMAGE_BG_DARK("Image", "Sample Wallpaper", ThemeType.IMAGE, R.drawable.ic_wallpaper_dummy);
+ DEFAULT("Default", "Classic Adaptive", ThemeType.COLOR, R.color.color_bg_keyboard_default),
+ PURPLE("Frogo Purple", "Signature Brand", ThemeType.COLOR, R.color.color_bg_keyboard_purple),
+ DARK("Midnight AMOLED", "Deep OLED Black", ThemeType.COLOR, R.color.color_bg_keyboard_dark),
+ BLUE("Ocean Blue", "Calm & Focused", ThemeType.COLOR, R.color.color_bg_keyboard_blue),
+ GREEN("Forest Emerald", "Natural Harmony", ThemeType.COLOR, R.color.color_bg_keyboard_green),
+ RED("Crimson Sunset", "Vibrant Warmth", ThemeType.COLOR, R.color.color_bg_keyboard_red),
+ ORANGE("Sunset Orange", "Energetic Twilight", ThemeType.COLOR, R.color.color_bg_keyboard_orange),
+ CYAN("Nordic Cyan", "Fresh & Clean", ThemeType.COLOR, R.color.color_bg_keyboard_cyan),
+ PINK("Sakura Pink", "Aesthetic Pastel", ThemeType.COLOR, R.color.color_bg_keyboard_pink),
+ YELLOW("Amber Gold", "Golden Accent", ThemeType.COLOR, R.color.color_bg_keyboard_yellow),
+ IMAGE_BG_DARK("Wallpaper", "Sample Artwork", ThemeType.IMAGE, R.drawable.ic_wallpaper_dummy);
fun mapToModel(): KeyboardThemeModel {
return KeyboardThemeModel(
diff --git a/app/src/main/java/com/frogobox/appkeyboard/repository/autotext/AutoTextRepository.kt b/app/src/main/java/com/frogobox/appkeyboard/repository/autotext/AutoTextRepository.kt
index 526afe4..7097785 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/repository/autotext/AutoTextRepository.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/repository/autotext/AutoTextRepository.kt
@@ -4,7 +4,7 @@ import com.frogobox.appkeyboard.common.callback.DataResponseCallback
import com.frogobox.appkeyboard.common.callback.StateResponseCallback
import com.frogobox.appkeyboard.model.AutoTextEntity
import com.frogobox.appkeyboard.model.AutoTextLabelType
-
+import kotlinx.coroutines.flow.Flow
/**
* Created by Faisal Amir on 25/11/22
@@ -13,6 +13,28 @@ import com.frogobox.appkeyboard.model.AutoTextLabelType
interface AutoTextRepository {
+ fun getAutoText(): Flow>
+
+ fun getAutoTextByLabel(label: AutoTextLabelType): Flow>
+
+ fun getAutoTextByTitle(title: String): Flow>
+
+ fun getAutoTextByBody(body: String): Flow>
+
+ fun getAutoTextByTitleOrBody(keyword: String): Flow>
+
+ suspend fun insertAutoText(autoText: AutoTextEntity)
+
+ suspend fun insertAutoText(autoTexts: List)
+
+ suspend fun updateAutoText(autoText: AutoTextEntity)
+
+ suspend fun deleteAutoText(autoText: AutoTextEntity)
+
+ suspend fun deleteAutoText(idList: List)
+
+ suspend fun nukeAutoText()
+
fun getAutoText(callback: DataResponseCallback>)
fun getAutoTextByLabel(label: AutoTextLabelType, callback: DataResponseCallback>)
diff --git a/app/src/main/java/com/frogobox/appkeyboard/repository/autotext/AutoTextRepositoryImpl.kt b/app/src/main/java/com/frogobox/appkeyboard/repository/autotext/AutoTextRepositoryImpl.kt
index 0c23b0a..5386eb8 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/repository/autotext/AutoTextRepositoryImpl.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/repository/autotext/AutoTextRepositoryImpl.kt
@@ -5,10 +5,12 @@ import com.frogobox.appkeyboard.common.callback.StateResponseCallback
import com.frogobox.appkeyboard.data.local.autotext.AutoTextDao
import com.frogobox.appkeyboard.model.AutoTextEntity
import com.frogobox.appkeyboard.model.AutoTextLabelType
-import com.frogobox.sdk.ext.executeRoomDB
-import com.frogobox.sdk.ext.fetchRoomDB
-import io.reactivex.rxjava3.disposables.CompositeDisposable
-import io.reactivex.rxjava3.disposables.Disposable
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import javax.inject.Inject
/**
@@ -16,24 +18,84 @@ import javax.inject.Inject
* https://github.com/amirisback
*/
-
class AutoTextRepositoryImpl @Inject constructor(
private val dao: AutoTextDao,
) : AutoTextRepository {
- private val compositeDisposable: CompositeDisposable by lazy { CompositeDisposable() }
+ override fun getAutoText(): Flow> {
+ return dao.getAll().flowOn(Dispatchers.IO)
+ }
+
+ override fun getAutoTextByLabel(label: AutoTextLabelType): Flow> {
+ return dao.getByLabel(label).flowOn(Dispatchers.IO)
+ }
+
+ override fun getAutoTextByTitle(title: String): Flow> {
+ return dao.getByTitle(title).flowOn(Dispatchers.IO)
+ }
+
+ override fun getAutoTextByBody(body: String): Flow> {
+ return dao.getByBody(body).flowOn(Dispatchers.IO)
+ }
+
+ override fun getAutoTextByTitleOrBody(keyword: String): Flow> {
+ return dao.getByTitleOrBody(keyword).flowOn(Dispatchers.IO)
+ }
+
+ override suspend fun insertAutoText(autoText: AutoTextEntity) {
+ withContext(Dispatchers.IO) {
+ dao.insert(autoText)
+ }
+ }
+
+ override suspend fun insertAutoText(autoTexts: List) {
+ withContext(Dispatchers.IO) {
+ dao.insert(autoTexts)
+ }
+ }
+
+ override suspend fun updateAutoText(autoText: AutoTextEntity) {
+ withContext(Dispatchers.IO) {
+ dao.update(autoText)
+ }
+ }
- fun onClearDisposables() {
- compositeDisposable.clear()
+ override suspend fun deleteAutoText(autoText: AutoTextEntity) {
+ withContext(Dispatchers.IO) {
+ dao.delete(autoText)
+ }
}
- fun addSubscribe(disposable: Disposable) {
- compositeDisposable.add(disposable)
+ override suspend fun deleteAutoText(idList: List) {
+ withContext(Dispatchers.IO) {
+ dao.delete(idList)
+ }
+ }
+
+ override suspend fun nukeAutoText() {
+ withContext(Dispatchers.IO) {
+ dao.nukeData()
+ }
}
override fun getAutoText(callback: DataResponseCallback>) {
- dao.getAll().fetchRoomDB(callback) {
- addSubscribe(it)
+ callback.onShowProgress()
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ dao.getAll().collect {
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onSuccess(it)
+ callback.onFinish()
+ }
+ }
+ } catch (e: Exception) {
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onFailed(-1, e.localizedMessage ?: "Unknown Error")
+ callback.onFinish()
+ }
+ }
}
}
@@ -41,8 +103,23 @@ class AutoTextRepositoryImpl @Inject constructor(
label: AutoTextLabelType,
callback: DataResponseCallback>,
) {
- dao.getByLabel(label).fetchRoomDB(callback) {
- addSubscribe(it)
+ callback.onShowProgress()
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ dao.getByLabel(label).collect {
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onSuccess(it)
+ callback.onFinish()
+ }
+ }
+ } catch (e: Exception) {
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onFailed(-1, e.localizedMessage ?: "Unknown Error")
+ callback.onFinish()
+ }
+ }
}
}
@@ -50,8 +127,23 @@ class AutoTextRepositoryImpl @Inject constructor(
title: String,
callback: DataResponseCallback>,
) {
- dao.getByTitle(title).fetchRoomDB(callback) {
- addSubscribe(it)
+ callback.onShowProgress()
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ dao.getByTitle(title).collect {
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onSuccess(it)
+ callback.onFinish()
+ }
+ }
+ } catch (e: Exception) {
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onFailed(-1, e.localizedMessage ?: "Unknown Error")
+ callback.onFinish()
+ }
+ }
}
}
@@ -59,8 +151,23 @@ class AutoTextRepositoryImpl @Inject constructor(
body: String,
callback: DataResponseCallback>,
) {
- dao.getByBody(body).fetchRoomDB(callback) {
- addSubscribe(it)
+ callback.onShowProgress()
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ dao.getByBody(body).collect {
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onSuccess(it)
+ callback.onFinish()
+ }
+ }
+ } catch (e: Exception) {
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onFailed(-1, e.localizedMessage ?: "Unknown Error")
+ callback.onFinish()
+ }
+ }
}
}
@@ -68,33 +175,144 @@ class AutoTextRepositoryImpl @Inject constructor(
keyword: String,
callback: DataResponseCallback>,
) {
- dao.getByTitleOrBody(keyword).fetchRoomDB(callback) {
- addSubscribe(it)
+ callback.onShowProgress()
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ dao.getByTitleOrBody(keyword).collect {
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onSuccess(it)
+ callback.onFinish()
+ }
+ }
+ } catch (e: Exception) {
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onFailed(-1, e.localizedMessage ?: "Unknown Error")
+ callback.onFinish()
+ }
+ }
}
}
override fun insertAutoText(autoText: AutoTextEntity, callback: StateResponseCallback) {
- dao.insert(autoText).executeRoomDB(callback)
+ callback.onShowProgress()
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ dao.insert(autoText)
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onSuccess()
+ callback.onFinish()
+ }
+ } catch (e: Exception) {
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onFailed(-1, e.localizedMessage ?: "Unknown Error")
+ callback.onFinish()
+ }
+ }
+ }
}
override fun insertAutoText(autoTexts: List, callback: StateResponseCallback) {
- dao.insert(autoTexts).executeRoomDB(callback)
+ callback.onShowProgress()
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ dao.insert(autoTexts)
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onSuccess()
+ callback.onFinish()
+ }
+ } catch (e: Exception) {
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onFailed(-1, e.localizedMessage ?: "Unknown Error")
+ callback.onFinish()
+ }
+ }
+ }
}
override fun updateAutoText(autoText: AutoTextEntity, callback: StateResponseCallback) {
- dao.update(autoText).executeRoomDB(callback)
+ callback.onShowProgress()
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ dao.update(autoText)
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onSuccess()
+ callback.onFinish()
+ }
+ } catch (e: Exception) {
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onFailed(-1, e.localizedMessage ?: "Unknown Error")
+ callback.onFinish()
+ }
+ }
+ }
}
override fun deleteAutoText(autoText: AutoTextEntity, callback: StateResponseCallback) {
- dao.delete(autoText).executeRoomDB(callback)
+ callback.onShowProgress()
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ dao.delete(autoText)
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onSuccess()
+ callback.onFinish()
+ }
+ } catch (e: Exception) {
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onFailed(-1, e.localizedMessage ?: "Unknown Error")
+ callback.onFinish()
+ }
+ }
+ }
}
override fun deleteAutoText(idList: List, callback: StateResponseCallback) {
- dao.delete(idList).executeRoomDB(callback)
+ callback.onShowProgress()
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ dao.delete(idList)
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onSuccess()
+ callback.onFinish()
+ }
+ } catch (e: Exception) {
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onFailed(-1, e.localizedMessage ?: "Unknown Error")
+ callback.onFinish()
+ }
+ }
+ }
}
override fun nukeAutoText(callback: StateResponseCallback) {
- dao.nukeData().executeRoomDB(callback)
+ callback.onShowProgress()
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ dao.nukeData()
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onSuccess()
+ callback.onFinish()
+ }
+ } catch (e: Exception) {
+ withContext(Dispatchers.Main) {
+ callback.onHideProgress()
+ callback.onFailed(-1, e.localizedMessage ?: "Unknown Error")
+ callback.onFinish()
+ }
+ }
+ }
}
}
\ 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 74ccc37..5276f7a 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardIME.kt
@@ -5,6 +5,7 @@ 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
import com.frogobox.appkeyboard.R
@@ -15,6 +16,9 @@ import com.frogobox.appkeyboard.model.KeyboardFeatureType
import com.frogobox.appkeyboard.model.ThemeType
import com.frogobox.appkeyboard.ui.main.MainActivity
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.main.ItemMainKeyboard
import com.frogobox.recycler.core.FrogoRecyclerNotifyListener
import com.frogobox.recycler.core.IFrogoBindingAdapter
import com.frogobox.recycler.ext.injectorBinding
@@ -22,6 +26,7 @@ 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 dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -36,10 +41,145 @@ class KeyboardIME : BaseKeyboardIME() {
@Inject
lateinit var keyboardUtil: KeyboardUtil
+ @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()
+ 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 -> {
+ 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 {
@@ -48,25 +188,35 @@ class KeyboardIME : BaseKeyboardIME() {
R.color.color_bg_keyboard_default
)
- val backgroundType = ThemeType.valueOf(
- pref.getPrefString(
- KeyboardUtil.KEYBOARD_COLOR_TYPE,
- ThemeType.COLOR.name
- )
+ 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)
}
}
}
}
+ override fun onWindowShown() {
+ super.onWindowShown()
+ applySoundAndHapticSettings()
+ }
+
override fun initialSetupKeyboard() {
+ applySoundAndHapticSettings()
binding?.keyboardMain?.setKeyboard(keyboard!!)
}
@@ -100,12 +250,14 @@ class KeyboardIME : BaseKeyboardIME() {
binding?.apply {
keyboardMain.invisible()
keyboardHeader.invisible()
+ keyboardSuggestion.gone()
}
}
override fun showMainKeyboard() {
binding?.apply {
keyboardMain.visible()
+ keyboardSuggestion.gone()
if (keyboardUtil.menuKeyboard().isEmpty()) {
keyboardHeader.gone()
} else {
@@ -197,170 +349,107 @@ class KeyboardIME : BaseKeyboardIME() {
} else {
keyboardHeader.visible()
keyboardHeader.injectorBinding()
- .addData(keyboardUtil.menuKeyboard()).addCallback(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()
- keyboardNews.visible()
- }
-
- KeyboardFeatureType.MOVIE -> {
- hideMainKeyboard()
- keyboardMoview.visible()
- }
-
- KeyboardFeatureType.WEB -> {
- keyboardHeader.gone()
- keyboardWebview.visible()
- }
-
- KeyboardFeatureType.FORM -> {
- keyboardHeader.gone()
- keyboardForm.visible()
- keyboardForm.binding.etText.showKeyboardExt()
- keyboardForm.binding.etText2.showKeyboardExt()
- keyboardForm.binding.etText3.showKeyboardExt()
-
- keyboardForm.setOnClickListener {
- hideOnlyKeyboard()
- }
- }
-
- KeyboardFeatureType.AUTO_TEXT -> {
- hideMainKeyboard()
- keyboardAutotext.visible()
- }
-
- KeyboardFeatureType.TEMPLATE_TEXT_GAME -> {
- hideMainKeyboard()
- keyboardTemplateText.setupTemplateTextType(KeyboardFeatureType.TEMPLATE_TEXT_GAME)
- keyboardTemplateText.visible()
- }
-
- KeyboardFeatureType.TEMPLATE_TEXT_APP -> {
- hideMainKeyboard()
- keyboardTemplateText.setupTemplateTextType(KeyboardFeatureType.TEMPLATE_TEXT_APP)
- keyboardTemplateText.visible()
- }
-
- KeyboardFeatureType.TEMPLATE_TEXT_SALE -> {
- hideMainKeyboard()
- keyboardTemplateText.setupTemplateTextType(KeyboardFeatureType.TEMPLATE_TEXT_SALE)
- keyboardTemplateText.visible()
- }
-
- KeyboardFeatureType.TEMPLATE_TEXT_LOVE -> {
- hideMainKeyboard()
- keyboardTemplateText.setupTemplateTextType(KeyboardFeatureType.TEMPLATE_TEXT_LOVE)
- keyboardTemplateText.visible()
- }
-
- KeyboardFeatureType.TEMPLATE_TEXT_GREETING -> {
- hideMainKeyboard()
- keyboardTemplateText.setupTemplateTextType(KeyboardFeatureType.TEMPLATE_TEXT_GREETING)
- keyboardTemplateText.visible()
- }
-
- 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)
- })
- }
-
- }
-
- }
-
- }).createLayoutGrid(gridSize).build()
+ .addData(keyboardUtil.menuKeyboard())
+ .addCallback(featureKeyboardCallback)
+ .createLayoutGrid(gridSize).build()
}
}
}
- override fun onKey(code: Int) {
+ private fun getActiveInputConnection(): InputConnection? {
val formView = binding?.keyboardForm
- var inputConnection = currentInputConnection
-
if (formView?.visibility == View.VISIBLE) {
val et1 = formView.binding.etText
- val et1Connection = et1.onCreateInputConnection(EditorInfo())
-
val et2 = formView.binding.etText2
- val et2Connection = et2.onCreateInputConnection(EditorInfo())
-
val et3 = formView.binding.etText3
- val et3Connection = et3.onCreateInputConnection(EditorInfo())
if (et1.isFocused) {
- inputConnection = et1Connection
+ return et1.onCreateInputConnection(EditorInfo())
} else if (et2.isFocused) {
- inputConnection = et2Connection
+ return et2.onCreateInputConnection(EditorInfo())
} else if (et3.isFocused) {
- inputConnection = et3Connection
+ return et3.onCreateInputConnection(EditorInfo())
}
-
} else if (binding?.keyboardWebview?.visibility == View.VISIBLE) {
- inputConnection =
- binding?.keyboardWebview?.binding?.webview?.onCreateInputConnection(EditorInfo())
- } else {
- inputConnection = currentInputConnection
+ return binding?.keyboardWebview?.binding?.webview?.onCreateInputConnection(EditorInfo())
+ }
+ 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()
+ }
+ }
+
+ private fun showFeatureHeader() {
+ binding?.apply {
+ keyboardSuggestion.gone()
+ if (keyboardUtil.menuKeyboard().isEmpty()) {
+ keyboardHeader.gone()
+ } else {
+ keyboardHeader.visible()
+ }
+ }
+ }
+
+ override fun onKey(code: Int) {
+ val ic = getActiveInputConnection() ?: return
+ onKeyExt(code, ic)
+
+ if (keyboardUtil.isSuggestionEnabled()) {
+ val word = getWordBeforeCursor(ic)
+ if (word.isNotEmpty()) {
+ val suggestions = suggestionEngine.getSuggestions(word)
+ binding?.keyboardSuggestion?.setSuggestions(suggestions)
+ showSuggestionBar()
+ } else {
+ binding?.keyboardSuggestion?.clearSuggestions()
+ if (code == ItemMainKeyboard.KEYCODE_SPACE ||
+ code == ItemMainKeyboard.KEYCODE_ENTER ||
+ code == ItemMainKeyboard.KEYCODE_DELETE) {
+ showFeatureHeader()
+ }
+ }
}
- onKeyExt(code, inputConnection)
}
override fun initView() {
+ suggestionEngine.loadDictionaryFromAsset(this)
+ setupSuggestionBar()
setupFeatureKeyboard()
initBackToMainKeyboard()
}
@@ -387,4 +476,24 @@ class KeyboardIME : BaseKeyboardIME() {
return pref.getPrefBoolean(key, true)
}
+ private fun applySoundAndHapticSettings() {
+ val soundEnabled = pref.getPrefBoolean(MechanicalSoundManager.PREF_KEYBOARD_SOUND_ENABLED, true)
+ val soundType = pref.getPrefString(
+ MechanicalSoundManager.PREF_KEYBOARD_SOUND_TYPE,
+ MechanicalSoundType.CHERRY_MX_BLUE.id
+ )
+ val soundVolumeInt = pref.getPrefInt(MechanicalSoundManager.PREF_KEYBOARD_SOUND_VOLUME, 80)
+ val vibrateEnabled = pref.getPrefBoolean(MechanicalSoundManager.PREF_KEYBOARD_VIBRATE_ENABLED, true)
+
+ ItemMainKeyboard.SOUND_ON_KEYPRESS = soundEnabled
+ ItemMainKeyboard.MECHANICAL_SOUND_TYPE = soundType
+ ItemMainKeyboard.SOUND_VOLUME = (soundVolumeInt / 100f).coerceIn(0.05f, 1.0f)
+ ItemMainKeyboard.VIBRATE_ON_KEYPRESS = vibrateEnabled
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ MechanicalSoundManager.getInstance(this).release()
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardUtil.kt b/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardUtil.kt
index bc97531..62f2a3e 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardUtil.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/services/KeyboardUtil.kt
@@ -22,14 +22,20 @@ class KeyboardUtil @Inject constructor(
const val KEYBOARD_TYPE = "KEYBOARD_TYPE"
const val KEYBOARD_COLOR = "KEYBOARD_COLOR"
const val KEYBOARD_COLOR_TYPE = "KEYBOARD_COLOR_TYPE"
+ const val KEYBOARD_SUGGESTION_ENABLED = "KEYBOARD_SUGGESTION_ENABLED"
}
private fun getStateToggle(key: String) : Boolean {
return pref.getPrefBoolean(key, true)
}
+ fun isSuggestionEnabled(): Boolean {
+ return getStateToggle(KeyboardFeatureType.SUGGESTION.id)
+ }
+
fun menuToggle(): List {
return listOf(
+ KeyboardFeatureType.SUGGESTION.mapToModel(),
KeyboardFeatureType.AUTO_TEXT.mapToModel(),
KeyboardFeatureType.TEMPLATE_TEXT_APP.mapToModel(),
KeyboardFeatureType.TEMPLATE_TEXT_GAME.mapToModel(),
@@ -55,14 +61,7 @@ class KeyboardUtil @Inject constructor(
}
fun keyboardTheme(): List {
- return listOf(
- KeyboardThemeType.DEFAULT.mapToModel(),
- KeyboardThemeType.RED.mapToModel(),
- KeyboardThemeType.GREEN.mapToModel(),
- KeyboardThemeType.YELLOW.mapToModel(),
- KeyboardThemeType.BLUE.mapToModel(),
- KeyboardThemeType.IMAGE_BG_DARK.mapToModel()
- )
+ return KeyboardThemeType.entries.map { it.mapToModel() }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/frogobox/appkeyboard/suggestion/KeyboardSuggestionView.kt b/app/src/main/java/com/frogobox/appkeyboard/suggestion/KeyboardSuggestionView.kt
new file mode 100644
index 0000000..c0a90b3
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/suggestion/KeyboardSuggestionView.kt
@@ -0,0 +1,115 @@
+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/suggestion/SuggestionResult.kt b/app/src/main/java/com/frogobox/appkeyboard/suggestion/SuggestionResult.kt
new file mode 100644
index 0000000..ff20c3a
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/suggestion/SuggestionResult.kt
@@ -0,0 +1,21 @@
+package com.frogobox.appkeyboard.suggestion
+
+/**
+ * Data class representing the 3-section candidate suggestion model
+ * satisfying GitHub Issue #50:
+ * - Section 1: userWord (literal verbatim word typed by user)
+ * - Section 2: predictedWord (highest frequency completion or contextual prediction)
+ * - Section 3: autoCorrectWord (best typographical correction or alternative)
+ */
+data class SuggestionResult(
+ val userWord: String = "",
+ val predictedWord: String = "",
+ val autoCorrectWord: String = ""
+) {
+ fun hasSuggestions(): Boolean =
+ userWord.isNotEmpty() || predictedWord.isNotEmpty() || autoCorrectWord.isNotEmpty()
+
+ companion object {
+ val EMPTY = SuggestionResult("", "", "")
+ }
+}
diff --git a/app/src/main/java/com/frogobox/appkeyboard/suggestion/WordSuggestionEngine.kt b/app/src/main/java/com/frogobox/appkeyboard/suggestion/WordSuggestionEngine.kt
new file mode 100644
index 0000000..85d7dde
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/suggestion/WordSuggestionEngine.kt
@@ -0,0 +1,294 @@
+package com.frogobox.appkeyboard.suggestion
+
+import android.content.Context
+import java.io.BufferedReader
+import java.io.InputStreamReader
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.CopyOnWriteArrayList
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlin.math.abs
+import kotlin.math.min
+
+/**
+ * Intelligent on-device Word Suggestion & Auto-Correction Engine
+ * Provides sub-millisecond candidate computation satisfying GitHub Issue #50:
+ * - Section 1: User's typed word (verbatim)
+ * - Section 2: Predicted word (prefix match / completion)
+ * - Section 3: Auto-correct word (edit distance / typo correction)
+ */
+@Singleton
+class WordSuggestionEngine @Inject constructor() {
+
+ // Common instant typo and shorthand substitution mapping
+ private val commonTypoMap = mapOf(
+ "teh" to "the",
+ "wht" to "what",
+ "adn" to "and",
+ "dont" to "don't",
+ "cant" to "can't",
+ "wont" to "won't",
+ "im" to "I'm",
+ "ive" to "I've",
+ "youre" to "you're",
+ "theyre" to "they're",
+ "alot" to "a lot",
+ "helo" to "hello",
+ "hw" to "how",
+ "pls" to "please",
+ "thx" to "thanks",
+ "ty" to "thank you",
+ "u" to "you",
+ "r" to "are",
+ "ur" to "your",
+ "recive" to "receive",
+ "recieve" to "receive",
+ "becuase" to "because",
+ "definately" to "definitely",
+ "seperate" to "separate",
+ "goverment" to "government",
+ "occured" to "occurred",
+ "untill" to "until",
+ "beleive" to "believe",
+ "frogo" to "Frogo"
+ )
+
+ // In-memory frequency-ranked dictionary
+ private val dictionaryList = CopyOnWriteArrayList()
+ private val dictionarySet = ConcurrentHashMap.newKeySet()
+ private val userCustomWords = ConcurrentHashMap.newKeySet()
+
+ @Volatile
+ private var isDictionaryLoaded = false
+
+ init {
+ loadBuiltinFallbackVocabulary()
+ }
+
+ /**
+ * Fallback high-frequency words loaded synchronously so engine works immediately
+ */
+ private fun loadBuiltinFallbackVocabulary() {
+ val fallbackWords = listOf(
+ "the", "be", "to", "of", "and", "a", "in", "that", "have", "i",
+ "it", "for", "not", "on", "with", "he", "as", "you", "do", "at",
+ "this", "but", "his", "by", "from", "they", "we", "say", "her", "she",
+ "or", "an", "will", "my", "one", "all", "would", "there", "their", "what",
+ "so", "up", "out", "if", "about", "who", "get", "which", "go", "me",
+ "when", "make", "can", "like", "time", "no", "just", "him", "know", "take",
+ "people", "into", "year", "your", "good", "some", "could", "them", "see", "other",
+ "than", "then", "now", "look", "only", "come", "its", "over", "think", "also",
+ "back", "after", "use", "two", "how", "our", "work", "first", "well", "way",
+ "even", "new", "want", "because", "any", "these", "give", "day", "most", "us",
+ "hello", "thanks", "please", "sorry", "today", "tomorrow", "keyboard", "android",
+ "apple", "application", "screen", "mobile", "message", "email", "happy", "great",
+ "help", "home", "call", "start", "stop", "test", "ready", "super", "cool", "fine"
+ )
+ for (w in fallbackWords) {
+ addWordInternal(w)
+ }
+ }
+
+ /**
+ * Loads extended dictionary from assets/text/dictionary_en.txt
+ */
+ fun loadDictionaryFromAsset(context: Context) {
+ if (isDictionaryLoaded) return
+ try {
+ val assetManager = context.assets
+ val inputStream = assetManager.open("text/dictionary_en.txt")
+ BufferedReader(InputStreamReader(inputStream)).use { reader ->
+ var line: String? = reader.readLine()
+ while (line != null) {
+ val word = line.trim().lowercase()
+ if (word.isNotEmpty() && !dictionarySet.contains(word)) {
+ addWordInternal(word)
+ }
+ line = reader.readLine()
+ }
+ }
+ isDictionaryLoaded = true
+ } catch (_: Exception) {
+ // Gracefully keep fallback vocabulary if asset read fails
+ }
+ }
+
+ private fun addWordInternal(word: String) {
+ if (!dictionarySet.contains(word)) {
+ dictionarySet.add(word)
+ dictionaryList.add(word)
+ }
+ }
+
+ /**
+ * Allows dynamically learning or adding user custom words
+ */
+ fun addUserWord(word: String) {
+ val trimmed = word.trim()
+ if (trimmed.length > 1) {
+ userCustomWords.add(trimmed.lowercase())
+ addWordInternal(trimmed.lowercase())
+ }
+ }
+
+ /**
+ * Core suggestion generation returning the 3 required sections:
+ * - Section 1: User's verbatim word
+ * - Section 2: Predicted completion / highest rank prediction
+ * - Section 3: Auto-corrected candidate or alternative suggestion
+ */
+ fun getSuggestions(input: String): SuggestionResult {
+ val trimmedInput = input.trim()
+ if (trimmedInput.isEmpty()) {
+ return SuggestionResult(
+ userWord = "",
+ predictedWord = matchCasing(input, "I"),
+ autoCorrectWord = matchCasing(input, "The")
+ )
+ }
+
+ val lowerQuery = trimmedInput.lowercase()
+ val userWord = trimmedInput
+
+ // 1. Direct typo replacement check
+ val typoMatch = commonTypoMap[lowerQuery]
+
+ // 2. Find prefix predictions
+ val prefixMatches = mutableListOf()
+ for (word in dictionaryList) {
+ if (word.startsWith(lowerQuery)) {
+ prefixMatches.add(word)
+ if (prefixMatches.size >= 5) break
+ }
+ }
+
+ // 3. Compute predicted word (Section 2)
+ val predictedWordRaw = when {
+ typoMatch != null -> typoMatch
+ prefixMatches.isNotEmpty() -> {
+ // If the exact word is typed and there is a longer continuation, use continuation
+ if (prefixMatches[0] == lowerQuery && prefixMatches.size > 1) {
+ prefixMatches[1]
+ } else {
+ prefixMatches[0]
+ }
+ }
+ else -> findClosestCorrection(lowerQuery) ?: lowerQuery
+ }
+
+ // 4. Compute auto-correct word (Section 3)
+ val autoCorrectWordRaw = when {
+ typoMatch != null -> {
+ // Typo was matched, provide alternative prefix or closest candidate
+ prefixMatches.firstOrNull { it != lowerQuery && it != typoMatch.lowercase() }
+ ?: findClosestCorrection(lowerQuery, exclude = listOf(typoMatch.lowercase()))
+ ?: lowerQuery
+ }
+ !dictionarySet.contains(lowerQuery) -> {
+ // Misspelled: find best correction different from predicted
+ findClosestCorrection(lowerQuery, exclude = listOf(predictedWordRaw.lowercase()))
+ ?: prefixMatches.firstOrNull { it != predictedWordRaw.lowercase() }
+ ?: lowerQuery
+ }
+ prefixMatches.size > 1 -> {
+ // Word is valid, provide alternative candidate
+ val secondOption = prefixMatches.firstOrNull { it != predictedWordRaw.lowercase() && it != lowerQuery }
+ secondOption ?: (prefixMatches.getOrNull(1) ?: lowerQuery)
+ }
+ else -> {
+ findClosestCorrection(lowerQuery, exclude = listOf(predictedWordRaw.lowercase()))
+ ?: lowerQuery
+ }
+ }
+
+ return SuggestionResult(
+ userWord = userWord,
+ predictedWord = matchCasing(trimmedInput, predictedWordRaw),
+ autoCorrectWord = matchCasing(trimmedInput, autoCorrectWordRaw)
+ )
+ }
+
+ /**
+ * Finds closest word in dictionary within Levenshtein distance <= 2
+ */
+ private fun findClosestCorrection(query: String, exclude: List = emptyList()): String? {
+ if (query.isEmpty()) return null
+ var bestMatch: String? = null
+ var minDistance = Int.MAX_VALUE
+
+ val maxAllowedDistance = if (query.length <= 3) 1 else 2
+
+ for (word in dictionaryList) {
+ if (exclude.contains(word) || word == query) continue
+
+ // Quick length filter: skip words with length difference > maxAllowedDistance
+ if (abs(word.length - query.length) > maxAllowedDistance) continue
+
+ // Bonus heuristic: prefer words sharing the first letter
+ val firstCharSame = word[0] == query[0]
+ val distance = calculateLevenshteinDistance(query, word)
+
+ val adjustedDistance = if (firstCharSame) distance else distance + 1
+
+ if (distance <= maxAllowedDistance && adjustedDistance < minDistance) {
+ minDistance = adjustedDistance
+ bestMatch = word
+ if (distance == 1 && firstCharSame) {
+ // Fast break on high-confidence match
+ break
+ }
+ }
+ }
+
+ return bestMatch
+ }
+
+ /**
+ * Efficient iterative Levenshtein Distance calculation
+ */
+ fun calculateLevenshteinDistance(s1: String, s2: String): Int {
+ val len1 = s1.length
+ val len2 = s2.length
+
+ var prev = IntArray(len2 + 1) { it }
+ var curr = IntArray(len2 + 1)
+
+ for (i in 1..len1) {
+ curr[0] = i
+ for (j in 1..len2) {
+ val cost = if (s1[i - 1] == s2[j - 1]) 0 else 1
+ curr[j] = min(
+ min(curr[j - 1] + 1, prev[j] + 1),
+ prev[j - 1] + cost
+ )
+ }
+ val temp = prev
+ prev = curr
+ curr = temp
+ }
+
+ return prev[len2]
+ }
+
+ /**
+ * Adapts candidate string casing to match the user's input style:
+ * - UPPERCASE if input is all caps (e.g., "HEL" -> "HELLO")
+ * - TitleCase if input is capitalized (e.g., "Hel" -> "Hello")
+ * - lowercase otherwise (e.g., "hel" -> "hello")
+ */
+ fun matchCasing(source: String, candidate: String): String {
+ if (source.isEmpty() || candidate.isEmpty()) return candidate
+ return when {
+ source.length > 1 && source.all { it.isLetter() && it.isUpperCase() } -> {
+ candidate.uppercase()
+ }
+ source.first().isUpperCase() -> {
+ candidate.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
+ }
+ else -> {
+ candidate.lowercase()
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextActivity.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextActivity.kt
index 11d60cd..12712f6 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextActivity.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextActivity.kt
@@ -2,141 +2,76 @@ package com.frogobox.appkeyboard.ui.autotext
import android.content.Intent
import android.os.Bundle
-import android.view.ViewGroup
-import androidx.activity.result.ActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
-import com.frogobox.appkeyboard.common.base.BaseActivity
-import com.frogobox.appkeyboard.databinding.ActivityAutotextBinding
-import com.frogobox.appkeyboard.databinding.ItemAutotextBinding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import com.frogobox.appkeyboard.common.base.BaseComposeActivity
import com.frogobox.appkeyboard.model.AutoTextEntity
import com.frogobox.coresdk.source.Resource
-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.showLogD
-import com.frogobox.sdk.ext.visible
import com.google.gson.Gson
import dagger.hilt.android.AndroidEntryPoint
-/**
- * Created by Faisal Amir on 11/03/23
- * https://github.com/amirisback
- */
-
-
@AndroidEntryPoint
-class AutoTextActivity : BaseActivity() {
+class AutoTextActivity : BaseComposeActivity() {
private val viewModel: AutoTextViewModel by viewModels()
- override fun setupViewBinding(): ActivityAutotextBinding {
- return ActivityAutotextBinding.inflate(layoutInflater)
- }
-
- override fun setupViewModel() {
- viewModel.apply {
- autoText.observe(this@AutoTextActivity) {
-
- showLogD("${it}", "AutoText")
-
- when (it) {
- is Resource.Error -> {}
- is Resource.Loading -> {}
- is Resource.Success -> {
- if (it.result.isEmpty()) {
- binding.emptyView.root.visible()
- binding.rvAutotext.gone()
- } else {
- binding.emptyView.root.gone()
- binding.rvAutotext.visible()
- setupRvAutoText(it.result)
- }
- }
- }
- }
- }
- }
-
- override fun onCreateExt(savedInstanceState: Bundle?) {
- super.onCreateExt(savedInstanceState)
- setupDetailActivity("Auto Text")
- setupUI()
- viewModel.getAutoText()
- }
-
- override fun setupActivityResultExt(result: ActivityResult) {
- super.setupActivityResultExt(result)
+ private var autoTextList by mutableStateOf>(emptyList())
+ private var isLoading by mutableStateOf(false)
+ private val activityResultLauncher = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result ->
when (result.resultCode) {
-
AutoTextDetailActivity.RESULT_CODE_DELETE,
AutoTextEditorActivity.RESULT_CODE_UPDATE,
- AutoTextEditorActivity.RESULT_CODE_ADD,
- -> {
+ AutoTextEditorActivity.RESULT_CODE_ADD -> {
viewModel.getAutoText()
}
-
}
}
- private fun setupUI() {
- binding.apply {
- btnAdd.setOnClickListener {
- startActivityResultExt(Intent(this@AutoTextActivity, AutoTextEditorActivity::class.java))
- }
- }
- }
-
- private fun setupRvAutoText(data: List) {
- binding.rvAutotext.injectorBinding()
- .addData(data)
- .addCallback(object : IFrogoBindingAdapter {
- 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): ItemAutotextBinding {
- return ItemAutotextBinding.inflate(layoutInflater, parent, false)
+ override fun onCreateExt(savedInstanceState: Bundle?) {
+ super.onCreateExt(savedInstanceState)
+ viewModel.autoText.observe(this) { resource ->
+ when (resource) {
+ is Resource.Loading -> {
+ isLoading = true
}
-
- override fun setupInitComponent(
- binding: ItemAutotextBinding,
- data: AutoTextEntity,
- position: Int,
- notifyListener: FrogoRecyclerNotifyListener,
- ) {
- binding.tvAutotextTitle.text = data.title
- binding.tvAutotextContent.text = data.body
+ is Resource.Success -> {
+ isLoading = false
+ autoTextList = resource.result
}
-
- override fun onItemClicked(
- binding: ItemAutotextBinding,
- data: AutoTextEntity,
- position: Int,
- notifyListener: FrogoRecyclerNotifyListener,
- ) {
- val extra = Gson().toJson(data)
- startActivityResultExt(
- Intent(this@AutoTextActivity, AutoTextDetailActivity::class.java).apply {
- putExtra(AutoTextDetailActivity.EXTRA_AUTO_TEXT, extra)
- }
- )
+ is Resource.Error -> {
+ isLoading = false
}
-
- })
- .createLayoutLinearVertical(false)
- .build()
+ }
+ }
+ viewModel.getAutoText()
}
+ @Composable
+ override fun Content() {
+ AutoTextScreen(
+ autoTextList = autoTextList,
+ isLoading = isLoading,
+ onItemClick = { data ->
+ val extra = Gson().toJson(data)
+ activityResultLauncher.launch(
+ Intent(this@AutoTextActivity, AutoTextDetailActivity::class.java).apply {
+ putExtra(AutoTextDetailActivity.EXTRA_AUTO_TEXT, extra)
+ }
+ )
+ },
+ onAddClick = {
+ activityResultLauncher.launch(
+ Intent(this@AutoTextActivity, AutoTextEditorActivity::class.java)
+ )
+ },
+ onBackClick = { finish() }
+ )
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextDetailActivity.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextDetailActivity.kt
index 654dbf5..c3537da 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextDetailActivity.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextDetailActivity.kt
@@ -2,22 +2,19 @@ package com.frogobox.appkeyboard.ui.autotext
import android.content.Intent
import android.os.Bundle
-import androidx.activity.result.ActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
-import com.frogobox.appkeyboard.common.base.BaseActivity
-import com.frogobox.appkeyboard.databinding.ActivityAutotextDetailBinding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import com.frogobox.appkeyboard.common.base.BaseComposeActivity
import com.frogobox.appkeyboard.model.AutoTextEntity
import com.google.gson.Gson
import dagger.hilt.android.AndroidEntryPoint
-/**
- * Created by Faisal Amir on 11/03/23
- * https://github.com/amirisback
- */
-
-
@AndroidEntryPoint
-class AutoTextDetailActivity : BaseActivity() {
+class AutoTextDetailActivity : BaseComposeActivity() {
companion object {
const val EXTRA_AUTO_TEXT = "EXTRA_AUTO_TEXT"
@@ -26,33 +23,29 @@ class AutoTextDetailActivity : BaseActivity() {
private val viewModel: AutoTextViewModel by viewModels()
- override fun setupViewModel() {
- super.setupViewModel()
- viewModel.eventSuccessState.observe(this) {
- setResult(RESULT_CODE_DELETE)
- finish()
- }
- }
+ private var currentData by mutableStateOf(AutoTextEntity())
- override fun setupViewBinding(): ActivityAutotextDetailBinding {
- return ActivityAutotextDetailBinding.inflate(layoutInflater)
+ private val editLauncher = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ if (result.resultCode == AutoTextEditorActivity.RESULT_CODE_UPDATE) {
+ val json = result.data?.getStringExtra(AutoTextEditorActivity.EXTRA_AUTO_TEXT_EDIT_RESULT)
+ if (!json.isNullOrBlank()) {
+ val updatedData = Gson().fromJson(json, AutoTextEntity::class.java)
+ if (updatedData != null) {
+ currentData = updatedData
+ }
+ }
+ }
}
override fun onCreateExt(savedInstanceState: Bundle?) {
super.onCreateExt(savedInstanceState)
- setupDetailActivity("Detail Auto Text")
- setupUI(autoText())
- }
+ currentData = extractAutoText()
- override fun setupActivityResultExt(result: ActivityResult) {
- super.setupActivityResultExt(result)
- if (result.resultCode == AutoTextEditorActivity.RESULT_CODE_UPDATE) {
- result.data?.getStringExtra(AutoTextEditorActivity.EXTRA_AUTO_TEXT_EDIT_RESULT)
- val data = Gson().fromJson(
- result.data?.getStringExtra(AutoTextEditorActivity.EXTRA_AUTO_TEXT_EDIT_RESULT),
- AutoTextEntity::class.java
- )
- setupUI(data)
+ viewModel.eventSuccessState.observe(this) {
+ setResult(RESULT_CODE_DELETE)
+ finish()
}
}
@@ -61,42 +54,37 @@ class AutoTextDetailActivity : BaseActivity() {
super.doOnBackPressedExt()
}
-
- private fun autoText(): AutoTextEntity {
+ private fun extractAutoText(): AutoTextEntity {
return if (intent.hasExtra(EXTRA_AUTO_TEXT)) {
Gson().fromJson(
intent.extras?.getString(EXTRA_AUTO_TEXT),
AutoTextEntity::class.java
- )
+ ) ?: AutoTextEntity()
} else {
AutoTextEntity()
}
}
- private fun setupUI(data: AutoTextEntity) {
- binding.apply {
-
- tvTitle.text = data.title
- tvContent.text = data.body
-
- btnDelete.setOnClickListener {
- viewModel.deleteAutoText(data)
- }
-
- btnEdit.setOnClickListener {
- startActivityResultExt(
- Intent(
- this@AutoTextDetailActivity,
- AutoTextEditorActivity::class.java
- ).apply {
+ @Composable
+ override fun Content() {
+ AutoTextDetailScreen(
+ data = currentData,
+ onEditClick = {
+ editLauncher.launch(
+ Intent(this@AutoTextDetailActivity, AutoTextEditorActivity::class.java).apply {
putExtra(
AutoTextEditorActivity.EXTRA_AUTO_TEXT_EDIT,
- Gson().toJson(data)
+ Gson().toJson(currentData)
)
}
)
+ },
+ onDeleteClick = {
+ viewModel.deleteAutoText(currentData)
+ },
+ onBackClick = {
+ doOnBackPressedExt()
}
- }
+ )
}
-
}
\ No newline at end of file
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
new file mode 100644
index 0000000..7b47779
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextDetailScreen.kt
@@ -0,0 +1,429 @@
+package com.frogobox.appkeyboard.ui.autotext
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.widget.Toast
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+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.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.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ShortText
+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.Share
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+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
+import androidx.compose.material3.TextButton
+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
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.frogobox.appkeyboard.model.AutoTextEntity
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoKeyboardTheme
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoPrimary
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoStatusFailed
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoTopAppBar
+
+@Composable
+fun AutoTextDetailScreen(
+ data: AutoTextEntity,
+ onEditClick: () -> Unit,
+ onDeleteClick: () -> Unit,
+ onBackClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ var showDeleteDialog by remember { mutableStateOf(false) }
+ val scrollState = rememberScrollState()
+ val context = LocalContext.current
+
+ val wordCount = remember(data.body) {
+ if (data.body.isBlank()) 0
+ else data.body.trim().split(Regex("\\s+")).count { it.isNotEmpty() }
+ }
+
+ val onCopySnippet = {
+ val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText(data.title, data.body)
+ clipboard.setPrimaryClip(clip)
+ Toast.makeText(context, "Copied \"${data.title}\" to clipboard", Toast.LENGTH_SHORT).show()
+ }
+
+ val onShareSnippet = {
+ val sendIntent = Intent().apply {
+ action = Intent.ACTION_SEND
+ putExtra(Intent.EXTRA_TEXT, data.body)
+ type = "text/plain"
+ }
+ val shareIntent = Intent.createChooser(sendIntent, "Share Auto Text Snippet")
+ context.startActivity(shareIntent)
+ }
+
+ Scaffold(
+ modifier = modifier.fillMaxSize(),
+ topBar = {
+ FrogoTopAppBar(
+ title = "Detail Auto Text",
+ onBackClick = onBackClick
+ )
+ },
+ bottomBar = {
+ Surface(
+ color = MaterialTheme.colorScheme.surface,
+ tonalElevation = 3.dp,
+ shadowElevation = 8.dp
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ // Delete Button
+ OutlinedButton(
+ onClick = { showDeleteDialog = true },
+ modifier = Modifier
+ .weight(1f)
+ .height(52.dp),
+ shape = RoundedCornerShape(16.dp),
+ colors = ButtonDefaults.outlinedButtonColors(
+ contentColor = FrogoStatusFailed
+ ),
+ border = BorderStroke(1.dp, FrogoStatusFailed.copy(alpha = 0.4f))
+ ) {
+ Icon(
+ imageVector = Icons.Default.Delete,
+ contentDescription = "Delete",
+ modifier = Modifier.size(18.dp)
+ )
+ Spacer(modifier = Modifier.width(6.dp))
+ Text(
+ text = "Delete",
+ style = MaterialTheme.typography.labelLarge.copy(
+ fontSize = 15.sp,
+ fontWeight = FontWeight.SemiBold
+ ),
+ color = FrogoStatusFailed
+ )
+ }
+
+ // Edit Button
+ Button(
+ onClick = onEditClick,
+ modifier = Modifier
+ .weight(1f)
+ .height(52.dp),
+ shape = RoundedCornerShape(16.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = FrogoPrimary
+ )
+ ) {
+ Icon(
+ imageVector = Icons.Default.Edit,
+ contentDescription = "Edit",
+ tint = Color.White,
+ modifier = Modifier.size(18.dp)
+ )
+ Spacer(modifier = Modifier.width(6.dp))
+ Text(
+ text = "Edit",
+ style = MaterialTheme.typography.labelLarge.copy(
+ fontSize = 15.sp,
+ fontWeight = FontWeight.SemiBold
+ ),
+ color = Color.White
+ )
+ }
+ }
+ }
+ },
+ containerColor = MaterialTheme.colorScheme.background
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .verticalScroll(scrollState)
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ // Header Hero Card
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(20.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ ),
+ border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Box(
+ modifier = Modifier
+ .size(44.dp)
+ .clip(RoundedCornerShape(12.dp))
+ .background(FrogoPrimary.copy(alpha = 0.12f)),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ShortText,
+ contentDescription = null,
+ tint = FrogoPrimary,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+
+ IconButton(onClick = onShareSnippet) {
+ Icon(
+ imageVector = Icons.Default.Share,
+ contentDescription = "Share",
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(14.dp))
+
+ Text(
+ text = data.title,
+ style = MaterialTheme.typography.titleLarge.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 22.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurface
+ )
+
+ Spacer(modifier = Modifier.height(10.dp))
+
+ // Metadata Row
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Surface(
+ shape = RoundedCornerShape(8.dp),
+ color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f)
+ ) {
+ Text(
+ text = "${data.body.length} characters",
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
+ )
+ }
+
+ Surface(
+ shape = RoundedCornerShape(8.dp),
+ color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f)
+ ) {
+ Text(
+ text = "$wordCount words",
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // Primary Copy Button
+ Button(
+ onClick = onCopySnippet,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(46.dp),
+ shape = RoundedCornerShape(12.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = FrogoPrimary.copy(alpha = 0.12f),
+ contentColor = FrogoPrimary
+ ),
+ elevation = ButtonDefaults.buttonElevation(0.dp, 0.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Default.ContentCopy,
+ contentDescription = "Copy text",
+ modifier = Modifier.size(18.dp)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = "Copy to Clipboard",
+ style = MaterialTheme.typography.labelLarge.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 14.sp
+ )
+ )
+ }
+ }
+ }
+
+ // Snippet Content Card
+ Column(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(
+ text = "EXPANDED CONTENT",
+ style = MaterialTheme.typography.labelMedium.copy(
+ fontWeight = FontWeight.Bold,
+ letterSpacing = 1.sp,
+ fontSize = 12.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(start = 4.dp, bottom = 8.dp)
+ )
+
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(16.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ ),
+ border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp)
+ ) {
+ Text(
+ text = data.body,
+ style = MaterialTheme.typography.bodyLarge.copy(
+ fontSize = 15.sp,
+ lineHeight = 24.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ }
+ }
+ }
+ }
+ }
+
+ if (showDeleteDialog) {
+ AlertDialog(
+ onDismissRequest = { showDeleteDialog = false },
+ icon = {
+ Box(
+ modifier = Modifier
+ .size(48.dp)
+ .clip(CircleShape)
+ .background(FrogoStatusFailed.copy(alpha = 0.12f)),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.Delete,
+ contentDescription = null,
+ tint = FrogoStatusFailed,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+ },
+ title = {
+ Text(
+ text = "Delete 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.")
+ },
+ confirmButton = {
+ Button(
+ onClick = {
+ showDeleteDialog = false
+ onDeleteClick()
+ },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = FrogoStatusFailed
+ ),
+ shape = RoundedCornerShape(10.dp)
+ ) {
+ Text(
+ text = "Delete",
+ color = Color.White,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ },
+ dismissButton = {
+ TextButton(
+ onClick = { showDeleteDialog = false }
+ ) {
+ Text("Cancel")
+ }
+ },
+ shape = RoundedCornerShape(16.dp)
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun AutoTextDetailScreenPreview() {
+ FrogoKeyboardTheme(darkTheme = false) {
+ AutoTextDetailScreen(
+ data = AutoTextEntity(
+ id = 1,
+ title = "Greeting Message",
+ body = "Hello! Thanks for reaching out. How can I assist you with your project today?"
+ ),
+ onEditClick = {},
+ onDeleteClick = {},
+ onBackClick = {}
+ )
+ }
+}
diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextEditorActivity.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextEditorActivity.kt
index 719f80f..5728ddd 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextEditorActivity.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextEditorActivity.kt
@@ -3,21 +3,14 @@ package com.frogobox.appkeyboard.ui.autotext
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
-import com.frogobox.appkeyboard.common.base.BaseActivity
-import com.frogobox.appkeyboard.databinding.ActivityAutotextEditorBinding
+import androidx.compose.runtime.Composable
+import com.frogobox.appkeyboard.common.base.BaseComposeActivity
import com.frogobox.appkeyboard.model.AutoTextEntity
-import com.frogobox.sdk.ext.toText
import com.google.gson.Gson
import dagger.hilt.android.AndroidEntryPoint
-/**
- * Created by Faisal Amir on 11/03/23
- * https://github.com/amirisback
- */
-
-
@AndroidEntryPoint
-class AutoTextEditorActivity : BaseActivity() {
+class AutoTextEditorActivity : BaseComposeActivity() {
companion object {
const val EXTRA_AUTO_TEXT_EDIT = "EXTRA_AUTO_TEXT_EDIT"
@@ -28,77 +21,59 @@ class AutoTextEditorActivity : BaseActivity() {
private val viewModel: AutoTextViewModel by viewModels()
- override fun setupViewBinding(): ActivityAutotextEditorBinding {
- return ActivityAutotextEditorBinding.inflate(layoutInflater)
- }
-
- override fun setupViewModel() {
- super.setupViewModel()
- viewModel.apply {
- eventSuccessState.observe(this@AutoTextEditorActivity) {
- if (hasExtraDetail()) {
- setResult(RESULT_CODE_UPDATE, Intent().apply {
- val extra = AutoTextEntity(
- id = autoText().id,
- title = binding.etTitle.toText(),
- body = binding.etContent.toText()
- )
- putExtra(EXTRA_AUTO_TEXT_EDIT_RESULT, Gson().toJson(extra))
- })
- } else {
- setResult(RESULT_CODE_ADD)
- }
- finish()
- }
- }
- }
+ private var lastSavedTitle: String = ""
+ private var lastSavedBody: String = ""
override fun onCreateExt(savedInstanceState: Bundle?) {
super.onCreateExt(savedInstanceState)
- setupDetailActivity("Editor Auto Text")
- setupUI()
- }
-
- private fun setupUI() {
- binding.apply {
+ viewModel.eventSuccessState.observe(this) {
if (hasExtraDetail()) {
- btnSave.text = "Update"
- etTitle.setText(autoText().title)
- etContent.setText(autoText().body)
- }
-
- btnSave.setOnClickListener {
- if (hasExtraDetail()) {
- setupUpdate()
- } else {
- setupInsert()
- }
+ setResult(RESULT_CODE_UPDATE, Intent().apply {
+ val extra = AutoTextEntity(
+ id = autoText().id,
+ title = lastSavedTitle,
+ body = lastSavedBody
+ )
+ putExtra(EXTRA_AUTO_TEXT_EDIT_RESULT, Gson().toJson(extra))
+ })
+ } else {
+ setResult(RESULT_CODE_ADD)
}
+ finish()
}
}
- private fun autoText() : AutoTextEntity {
+ private fun autoText(): AutoTextEntity {
return Gson().fromJson(
intent.extras?.getString(EXTRA_AUTO_TEXT_EDIT),
AutoTextEntity::class.java
- )
+ ) ?: AutoTextEntity()
}
- private fun hasExtraDetail() : Boolean {
+ private fun hasExtraDetail(): Boolean {
return intent.hasExtra(EXTRA_AUTO_TEXT_EDIT)
}
- private fun setupInsert() {
- val title = binding.etTitle.text.toString()
- val body = binding.etContent.text.toString()
- viewModel.insertAutoText(title, body)
- }
-
- private fun setupUpdate() {
- val title = binding.etTitle.toText()
- val body = binding.etContent.toText()
- viewModel.updateAutoText(autoText().id, title, body)
+ @Composable
+ override fun Content() {
+ val detail = autoText()
+ val isEdit = hasExtraDetail()
+
+ AutoTextEditorScreen(
+ initialTitle = if (isEdit) detail.title else "",
+ initialBody = if (isEdit) detail.body else "",
+ isEditMode = isEdit,
+ onSave = { title, body ->
+ lastSavedTitle = title
+ lastSavedBody = body
+ if (isEdit) {
+ viewModel.updateAutoText(detail.id, title, body)
+ } else {
+ viewModel.insertAutoText(title, body)
+ }
+ },
+ onBackClick = { finish() }
+ )
}
-
}
\ No newline at end of file
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
new file mode 100644
index 0000000..959214f
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextEditorScreen.kt
@@ -0,0 +1,418 @@
+package com.frogobox.appkeyboard.ui.autotext
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+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.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.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.Notes
+import androidx.compose.material.icons.automirrored.filled.ShortText
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material.icons.filled.Keyboard
+import androidx.compose.material.icons.filled.Lightbulb
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+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.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.input.KeyboardCapitalization
+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.ui.theme.compose.FrogoKeyboardTheme
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoPrimary
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoTopAppBar
+
+@Composable
+fun AutoTextEditorScreen(
+ initialTitle: String,
+ initialBody: String,
+ isEditMode: Boolean,
+ onSave: (title: String, body: String) -> Unit,
+ onBackClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ var title by remember { mutableStateOf(initialTitle) }
+ var body by remember { mutableStateOf(initialBody) }
+ val isFormValid = title.isNotBlank() && body.isNotBlank()
+ val scrollState = rememberScrollState()
+
+ Scaffold(
+ modifier = modifier.fillMaxSize(),
+ topBar = {
+ FrogoTopAppBar(
+ title = if (isEditMode) "Edit Auto Text" else "New Auto Text",
+ onBackClick = onBackClick
+ )
+ },
+ bottomBar = {
+ Surface(
+ color = MaterialTheme.colorScheme.surface,
+ tonalElevation = 3.dp,
+ shadowElevation = 8.dp
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ Button(
+ onClick = {
+ if (isFormValid) {
+ onSave(title.trim(), body.trim())
+ }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(52.dp),
+ shape = RoundedCornerShape(16.dp),
+ enabled = isFormValid,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = FrogoPrimary
+ )
+ ) {
+ Icon(
+ imageVector = Icons.Default.Check,
+ contentDescription = null,
+ tint = Color.White,
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = if (isEditMode) "Update Template" else "Save Template",
+ style = MaterialTheme.typography.labelLarge.copy(
+ fontSize = 15.sp,
+ fontWeight = FontWeight.SemiBold
+ ),
+ color = Color.White
+ )
+ }
+ }
+ }
+ },
+ containerColor = MaterialTheme.colorScheme.background
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .verticalScroll(scrollState)
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ // Live Keyboard Preview Section
+ LiveKeyboardPreviewCard(
+ title = title,
+ body = body
+ )
+
+ // Title Input
+ OutlinedTextField(
+ value = title,
+ onValueChange = { if (it.length <= 50) title = it },
+ modifier = Modifier.fillMaxWidth(),
+ label = { Text("Shortcut Title") },
+ placeholder = { Text("e.g. Greeting, BCA Rekening, Email") },
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ShortText,
+ contentDescription = null,
+ tint = FrogoPrimary
+ )
+ },
+ trailingIcon = {
+ if (title.isNotEmpty()) {
+ IconButton(onClick = { title = "" }) {
+ Icon(
+ imageVector = Icons.Default.Clear,
+ contentDescription = "Clear Title",
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ },
+ supportingText = {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text("Appears in keyboard quick bar")
+ Text("${title.length} / 50")
+ }
+ },
+ singleLine = true,
+ shape = RoundedCornerShape(16.dp),
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.Sentences
+ ),
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = FrogoPrimary,
+ focusedLabelColor = FrogoPrimary,
+ focusedContainerColor = MaterialTheme.colorScheme.surface,
+ unfocusedContainerColor = MaterialTheme.colorScheme.surface
+ )
+ )
+
+ // Content / Body Input
+ OutlinedTextField(
+ value = body,
+ onValueChange = { body = it },
+ modifier = Modifier.fillMaxWidth(),
+ label = { Text("Template Content") },
+ placeholder = { Text("Enter the full text that will be inserted when tapped...") },
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.Notes,
+ contentDescription = null,
+ tint = FrogoPrimary
+ )
+ },
+ trailingIcon = {
+ if (body.isNotEmpty()) {
+ IconButton(onClick = { body = "" }) {
+ Icon(
+ imageVector = Icons.Default.Clear,
+ contentDescription = "Clear Content",
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ },
+ supportingText = {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text("Will be committed to input field")
+ Text("${body.length} characters")
+ }
+ },
+ minLines = 6,
+ shape = RoundedCornerShape(16.dp),
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.Sentences
+ ),
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = FrogoPrimary,
+ focusedLabelColor = FrogoPrimary,
+ focusedContainerColor = MaterialTheme.colorScheme.surface,
+ unfocusedContainerColor = MaterialTheme.colorScheme.surface
+ )
+ )
+
+ // Helper Guidance Card
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = FrogoPrimary.copy(alpha = 0.07f)
+ ),
+ border = BorderStroke(1.dp, FrogoPrimary.copy(alpha = 0.20f))
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(14.dp),
+ verticalAlignment = Alignment.Top
+ ) {
+ Box(
+ modifier = Modifier
+ .size(32.dp)
+ .clip(CircleShape)
+ .background(FrogoPrimary.copy(alpha = 0.16f)),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.Lightbulb,
+ contentDescription = null,
+ tint = FrogoPrimary,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ Column {
+ Text(
+ text = "Keyboard Tip",
+ style = MaterialTheme.typography.labelMedium.copy(
+ fontWeight = FontWeight.Bold
+ ),
+ color = FrogoPrimary
+ )
+ 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.",
+ style = MaterialTheme.typography.bodySmall.copy(
+ lineHeight = 16.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ }
+}
+
+@Composable
+private fun LiveKeyboardPreviewCard(
+ title: String,
+ body: String,
+ modifier: Modifier = Modifier
+) {
+ Card(
+ modifier = modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(16.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)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ imageVector = Icons.Default.Keyboard,
+ contentDescription = null,
+ tint = FrogoPrimary,
+ modifier = Modifier.size(18.dp)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = "LIVE KEYBOARD PREVIEW",
+ style = MaterialTheme.typography.labelMedium.copy(
+ fontWeight = FontWeight.Bold,
+ letterSpacing = 0.8.sp,
+ fontSize = 11.sp
+ ),
+ color = FrogoPrimary
+ )
+ }
+
+ Surface(
+ shape = RoundedCornerShape(6.dp),
+ color = FrogoPrimary.copy(alpha = 0.12f)
+ ) {
+ Text(
+ text = "Interactive",
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 10.sp,
+ color = FrogoPrimary
+ ),
+ modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ // Simulated Keyboard Item Box
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(12.dp))
+ .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
+ .padding(12.dp)
+ ) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(10.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))
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 14.dp, vertical = 10.dp)
+ ) {
+ Text(
+ text = if (title.isBlank()) "Template Title" else title,
+ style = MaterialTheme.typography.titleSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 14.sp
+ ),
+ color = if (title.isBlank()) MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) else MaterialTheme.colorScheme.onSurface,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ Spacer(modifier = Modifier.height(2.dp))
+
+ Text(
+ text = if (body.isBlank()) "Expanded text preview will appear here..." else body,
+ style = MaterialTheme.typography.bodySmall.copy(
+ fontSize = 12.sp
+ ),
+ color = if (body.isBlank()) MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) else MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun AutoTextEditorScreenPreview() {
+ FrogoKeyboardTheme(darkTheme = false) {
+ AutoTextEditorScreen(
+ initialTitle = "Quick Greeting",
+ initialBody = "Hello there! Wishing you a great day ahead.",
+ isEditMode = true,
+ onSave = { _, _ -> },
+ onBackClick = {}
+ )
+ }
+}
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
new file mode 100644
index 0000000..8da8755
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextScreen.kt
@@ -0,0 +1,495 @@
+package com.frogobox.appkeyboard.ui.autotext
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.widget.Toast
+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.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.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.Notes
+import androidx.compose.material.icons.automirrored.filled.ShortText
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material.icons.filled.ContentCopy
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.SearchOff
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExtendedFloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+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
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+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 com.frogobox.appkeyboard.model.AutoTextEntity
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoEmptyView
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoKeyboardTheme
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoPrimary
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoTopAppBar
+
+@Composable
+fun AutoTextScreen(
+ autoTextList: List,
+ isLoading: Boolean,
+ onItemClick: (AutoTextEntity) -> Unit,
+ onAddClick: () -> Unit,
+ onBackClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ var searchQuery by remember { mutableStateOf("") }
+ val context = LocalContext.current
+
+ val filteredList = remember(autoTextList, searchQuery) {
+ if (searchQuery.isBlank()) {
+ autoTextList
+ } else {
+ autoTextList.filter {
+ it.title.contains(searchQuery, ignoreCase = true) ||
+ it.body.contains(searchQuery, ignoreCase = true)
+ }
+ }
+ }
+
+ val onCopySnippet: (AutoTextEntity) -> Unit = { item ->
+ val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText(item.title, item.body)
+ clipboard.setPrimaryClip(clip)
+ Toast.makeText(context, "Copied \"${item.title}\" to clipboard", Toast.LENGTH_SHORT).show()
+ }
+
+ Scaffold(
+ modifier = modifier.fillMaxSize(),
+ topBar = {
+ FrogoTopAppBar(
+ title = "Auto Text",
+ onBackClick = onBackClick
+ )
+ },
+ floatingActionButton = {
+ ExtendedFloatingActionButton(
+ onClick = onAddClick,
+ containerColor = FrogoPrimary,
+ contentColor = Color.White,
+ shape = RoundedCornerShape(16.dp),
+ icon = {
+ Icon(
+ imageVector = Icons.Default.Add,
+ contentDescription = "Add Template"
+ )
+ },
+ text = {
+ Text(
+ text = "Add Template",
+ style = MaterialTheme.typography.labelLarge.copy(
+ fontSize = 14.sp,
+ fontWeight = FontWeight.SemiBold
+ )
+ )
+ }
+ )
+ },
+ containerColor = MaterialTheme.colorScheme.background
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ ) {
+ // Header Overview Banner & Search Bar
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 8.dp)
+ ) {
+ AutoTextHeaderBanner(
+ totalCount = autoTextList.size,
+ filteredCount = filteredList.size,
+ isSearching = searchQuery.isNotBlank()
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ // Modern Search Field
+ OutlinedTextField(
+ value = searchQuery,
+ onValueChange = { searchQuery = it },
+ modifier = Modifier.fillMaxWidth(),
+ placeholder = {
+ Text(
+ text = "Search templates by title or content...",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ },
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.Default.Search,
+ contentDescription = "Search",
+ tint = FrogoPrimary
+ )
+ },
+ trailingIcon = {
+ if (searchQuery.isNotEmpty()) {
+ IconButton(onClick = { searchQuery = "" }) {
+ Icon(
+ imageVector = Icons.Default.Clear,
+ contentDescription = "Clear Search",
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ },
+ singleLine = true,
+ shape = RoundedCornerShape(16.dp),
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = FrogoPrimary,
+ unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f),
+ focusedContainerColor = MaterialTheme.colorScheme.surface,
+ unfocusedContainerColor = MaterialTheme.colorScheme.surface
+ )
+ )
+ }
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .weight(1f)
+ ) {
+ when {
+ isLoading && autoTextList.isEmpty() -> {
+ CircularProgressIndicator(
+ modifier = Modifier.align(Alignment.Center),
+ color = FrogoPrimary
+ )
+ }
+ autoTextList.isEmpty() -> {
+ FrogoEmptyView(
+ title = "NO AUTO TEXT FOUND",
+ subtitle = "Tap 'Add Template' below to create a quick template.",
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+ filteredList.isEmpty() -> {
+ EmptySearchState(
+ query = searchQuery,
+ onClearSearch = { searchQuery = "" },
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+ else -> {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(
+ start = 16.dp,
+ end = 16.dp,
+ top = 4.dp,
+ bottom = 88.dp // Space for Extended FAB
+ ),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ items(filteredList, key = { it.id }) { item ->
+ AutoTextCard(
+ item = item,
+ onClick = { onItemClick(item) },
+ onCopy = { onCopySnippet(item) }
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun AutoTextHeaderBanner(
+ totalCount: Int,
+ filteredCount: Int,
+ isSearching: Boolean,
+ modifier: Modifier = Modifier
+) {
+ Card(
+ modifier = modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = FrogoPrimary.copy(alpha = 0.08f)
+ ),
+ border = BorderStroke(1.dp, FrogoPrimary.copy(alpha = 0.22f))
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(14.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.weight(1f)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(40.dp)
+ .clip(CircleShape)
+ .background(FrogoPrimary.copy(alpha = 0.16f)),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ShortText,
+ contentDescription = "Templates Icon",
+ tint = FrogoPrimary,
+ modifier = Modifier.size(22.dp)
+ )
+ }
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ Column {
+ Text(
+ text = "Quick Templates",
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 15.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Text(
+ text = "Instant shortcuts on your keyboard",
+ style = MaterialTheme.typography.bodySmall.copy(
+ fontSize = 12.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+
+ Surface(
+ shape = RoundedCornerShape(10.dp),
+ color = FrogoPrimary.copy(alpha = 0.16f)
+ ) {
+ Text(
+ text = if (isSearching) "$filteredCount / $totalCount" else "$totalCount Saved",
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 11.sp,
+ color = FrogoPrimary
+ ),
+ modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp)
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun AutoTextCard(
+ item: AutoTextEntity,
+ onClick: () -> Unit,
+ onCopy: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Card(
+ modifier = modifier
+ .fillMaxWidth()
+ .clickable { onClick() },
+ shape = RoundedCornerShape(16.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ ),
+ border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.weight(1f)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(36.dp)
+ .clip(RoundedCornerShape(10.dp))
+ .background(FrogoPrimary.copy(alpha = 0.12f)),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.Notes,
+ contentDescription = null,
+ tint = FrogoPrimary,
+ modifier = Modifier.size(20.dp)
+ )
+ }
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ Text(
+ text = item.title,
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 16.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurface,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+
+ IconButton(
+ onClick = onCopy,
+ modifier = Modifier.size(36.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Default.ContentCopy,
+ contentDescription = "Copy text",
+ tint = FrogoPrimary,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+
+ // Styled Snippet Container
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(10.dp))
+ .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f))
+ .padding(horizontal = 12.dp, vertical = 8.dp)
+ ) {
+ Text(
+ text = item.body,
+ style = MaterialTheme.typography.bodyMedium.copy(
+ fontSize = 13.sp,
+ lineHeight = 18.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun EmptySearchState(
+ query: String,
+ onClearSearch: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Box(
+ modifier = Modifier
+ .size(64.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.surfaceVariant),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.SearchOff,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.size(32.dp)
+ )
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ text = "No templates found",
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.Bold
+ ),
+ color = MaterialTheme.colorScheme.onSurface
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ Text(
+ text = "No template matches \"$query\"",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ TextButton(onClick = onClearSearch) {
+ Text(
+ text = "Clear Search",
+ color = FrogoPrimary,
+ fontWeight = FontWeight.SemiBold
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun AutoTextScreenPreview() {
+ FrogoKeyboardTheme(darkTheme = false) {
+ AutoTextScreen(
+ autoTextList = listOf(
+ AutoTextEntity(id = 1, title = "Greeting", body = "Hello, good morning! How can I help you today?"),
+ AutoTextEntity(id = 2, title = "Account Number", body = "BCA: 1234567890 a/n Faisal Amir")
+ ),
+ isLoading = false,
+ onItemClick = {},
+ onAddClick = {},
+ onBackClick = {}
+ )
+ }
+}
diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextViewModel.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextViewModel.kt
index 79692da..f53c14a 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextViewModel.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/autotext/AutoTextViewModel.kt
@@ -2,15 +2,17 @@ package com.frogobox.appkeyboard.ui.autotext
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
import com.frogobox.appkeyboard.common.base.BaseViewModel
-import com.frogobox.appkeyboard.common.callback.DataResponseCallback
-import com.frogobox.appkeyboard.common.callback.StateResponseCallback
import com.frogobox.appkeyboard.common.ext.getTimeNow
import com.frogobox.appkeyboard.model.AutoTextEntity
import com.frogobox.appkeyboard.model.AutoTextLabelType
import com.frogobox.appkeyboard.repository.autotext.AutoTextRepository
import com.frogobox.coresdk.source.Resource
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
import javax.inject.Inject
/**
@@ -53,23 +55,18 @@ class AutoTextViewModel @Inject constructor(
fun getAutoText() {
- repository.getAutoText(object : DataResponseCallback> {
- override fun onFinish() {}
- override fun onHideProgress() {}
-
- override fun onFailed(statusCode: Int, errorMessage: String) {
- _autoText.postValue(Resource.Error(statusCode, errorMessage))
- }
-
- override fun onShowProgress() {
- _autoText.postValue(Resource.Loading())
- }
-
- override fun onSuccess(data: List) {
- _autoText.postValue(Resource.Success(data))
- }
-
- })
+ viewModelScope.launch {
+ repository.getAutoText()
+ .onStart {
+ _autoText.postValue(Resource.Loading())
+ }
+ .catch { e ->
+ _autoText.postValue(Resource.Error(-1, e.localizedMessage ?: "Unknown Error"))
+ }
+ .collect { data ->
+ _autoText.postValue(Resource.Success(data))
+ }
+ }
}
fun insertAutoText(title: String, body: String) {
@@ -80,54 +77,35 @@ class AutoTextViewModel @Inject constructor(
body = body,
isActive = true
)
- repository.insertAutoText(data, object : StateResponseCallback {
- override fun onFailed(statusCode: Int, errorMessage: String) {
- _eventFailed.postValue(errorMessage)
- }
-
- override fun onFinish() {
- _eventFinishState.postValue(true)
- }
-
- override fun onHideProgress() {
+ viewModelScope.launch {
+ _eventShowProgressState.postValue(true)
+ try {
+ repository.insertAutoText(data)
_eventShowProgressState.postValue(false)
-
- }
-
- override fun onShowProgress() {
- _eventShowProgressState.postValue(true)
-
- }
-
- override fun onSuccess() {
_eventSuccessState.postValue(true)
+ _eventFinishState.postValue(true)
+ } catch (e: Exception) {
+ _eventShowProgressState.postValue(false)
+ _eventFailed.postValue(e.localizedMessage ?: "Failed to insert AutoText")
+ _eventFinishState.postValue(true)
}
}
- )
}
fun deleteAutoText(data: AutoTextEntity) {
- repository.deleteAutoText(data, object : StateResponseCallback {
- override fun onFailed(statusCode: Int, errorMessage: String) {
- _eventFailed.postValue(errorMessage)
- }
-
- override fun onFinish() {
- _eventFinishState.postValue(true)
- }
-
- override fun onHideProgress() {
+ viewModelScope.launch {
+ _eventShowProgressState.postValue(true)
+ try {
+ repository.deleteAutoText(data)
_eventShowProgressState.postValue(false)
- }
-
- override fun onShowProgress() {
- _eventShowProgressState.postValue(true)
- }
-
- override fun onSuccess() {
_eventSuccessState.postValue(true)
+ _eventFinishState.postValue(true)
+ } catch (e: Exception) {
+ _eventShowProgressState.postValue(false)
+ _eventFailed.postValue(e.localizedMessage ?: "Failed to delete AutoText")
+ _eventFinishState.postValue(true)
}
- })
+ }
}
fun updateAutoText(id: Int, title: String, body: String) {
@@ -139,27 +117,19 @@ class AutoTextViewModel @Inject constructor(
body = body,
isActive = true
)
- repository.updateAutoText(data, object : StateResponseCallback {
- override fun onFailed(statusCode: Int, errorMessage: String) {
- _eventFailed.postValue(errorMessage)
- }
-
- override fun onFinish() {
- _eventFinishState.postValue(true)
- }
-
- override fun onHideProgress() {
+ viewModelScope.launch {
+ _eventShowProgressState.postValue(true)
+ try {
+ repository.updateAutoText(data)
_eventShowProgressState.postValue(false)
- }
-
- override fun onShowProgress() {
- _eventShowProgressState.postValue(true)
- }
-
- override fun onSuccess() {
_eventSuccessState.postValue(true)
+ _eventFinishState.postValue(true)
+ } catch (e: Exception) {
+ _eventShowProgressState.postValue(false)
+ _eventFailed.postValue(e.localizedMessage ?: "Failed to update AutoText")
+ _eventFinishState.postValue(true)
}
- })
+ }
}
}
\ No newline at end of file
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 c91974d..1cd69ef 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
@@ -1,13 +1,16 @@
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.ItemKeyboardNewsBinding
+import com.frogobox.appkeyboard.databinding.ItemKeyboardAutotextBinding
import com.frogobox.appkeyboard.databinding.KeyboardAutotextBinding
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
@@ -31,6 +34,12 @@ class AutoTextKeyboard(
super.initUI()
binding.apply {
tvToolbarTitle.text = "Auto Text"
+ toolbarManage.setOnClickListener {
+ val intent = Intent(context, AutoTextActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ context.startActivity(intent)
+ }
}
}
@@ -44,21 +53,27 @@ class AutoTextKeyboard(
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 {
+ val adapterCallback = object : IFrogoBindingAdapter {
override fun onItemClicked(
- binding: ItemKeyboardNewsBinding,
+ binding: ItemKeyboardAutotextBinding,
data: AutoTextEntity,
position: Int,
notifyListener: FrogoRecyclerNotifyListener,
) {
- // Your Clicked
val output = data.body
currentInputConnection?.commitText(output, 1)
}
override fun onItemLongClicked(
- binding: ItemKeyboardNewsBinding,
+ binding: ItemKeyboardAutotextBinding,
data: AutoTextEntity,
position: Int,
notifyListener: FrogoRecyclerNotifyListener,
@@ -79,26 +94,28 @@ class AutoTextKeyboard(
return oldItem.id == newItem.id
}
- override fun setViewBinding(parent: ViewGroup): ItemKeyboardNewsBinding {
- return ItemKeyboardNewsBinding.inflate(
+ override fun setViewBinding(parent: ViewGroup): ItemKeyboardAutotextBinding {
+ return ItemKeyboardAutotextBinding.inflate(
LayoutInflater.from(context),
parent,
- false)
+ false
+ )
}
override fun setupInitComponent(
- binding: ItemKeyboardNewsBinding,
+ binding: ItemKeyboardAutotextBinding,
data: AutoTextEntity,
position: Int,
notifyListener: FrogoRecyclerNotifyListener,
) {
binding.apply {
tvItemKeyboardMain.text = data.title
+ tvItemKeyboardBody.text = data.body
}
}
}
- rvKeyboardMain.injectorBinding()
+ rvKeyboardMain.injectorBinding()
.addData(data)
.createLayoutLinearVertical(false)
.addCallback(adapterCallback)
diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/autotext/AutoTextKeyboardViewModel.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/autotext/AutoTextKeyboardViewModel.kt
index 1c001b3..c9c6758 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/autotext/AutoTextKeyboardViewModel.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/keyboard/autotext/AutoTextKeyboardViewModel.kt
@@ -1,34 +1,42 @@
package com.frogobox.appkeyboard.ui.keyboard.autotext
import android.content.Context
-import com.frogobox.appkeyboard.common.callback.DataResponseCallback
import com.frogobox.appkeyboard.data.local.db.AppDatabase
import com.frogobox.appkeyboard.model.AutoTextEntity
import com.frogobox.appkeyboard.repository.autotext.AutoTextRepository
import com.frogobox.appkeyboard.repository.autotext.AutoTextRepositoryImpl
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
/**
* Created by Faisal Amir on 11/03/23
* https://github.com/amirisback
*/
-
class AutoTextKeyboardViewModel(val context: Context) {
- private fun getRepository() : AutoTextRepository {
+ private fun getRepository(): AutoTextRepository {
return AutoTextRepositoryImpl(AppDatabase.newInstance(context).autoTextDao())
}
+ fun getAutoTextFlow(): Flow> {
+ return getRepository().getAutoText()
+ }
+
fun getAutoText(onSuccessData: (List) -> Unit) {
- getRepository().getAutoText(object : DataResponseCallback> {
- override fun onFailed(statusCode: Int, errorMessage: String) {}
- override fun onFinish() {}
- override fun onHideProgress() {}
- override fun onShowProgress() {}
- override fun onSuccess(data: List) {
- onSuccessData(data)
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ getRepository().getAutoText().collect { data ->
+ withContext(Dispatchers.Main) {
+ onSuccessData(data)
+ }
+ }
+ } catch (_: Exception) {
}
- })
+ }
}
}
\ No newline at end of file
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 f3fb5b2..3a253e8 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
@@ -5,7 +5,6 @@ import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import com.frogobox.appkeyboard.databinding.KeyboardFormBinding
-import com.frogobox.appkeyboard.ui.keyboard.autotext.AutoTextKeyboardViewModel
import com.frogobox.appkeyboard.ui.keyboard.movie.MovieKeyboard
import com.frogobox.appkeyboard.util.KeyboardNavigationHelper
import com.frogobox.libkeyboard.common.core.BaseKeyboard
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 5844d78..6f3a610 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
@@ -87,7 +87,7 @@ class MovieKeyboard(
val output = "Movie: ${data.title} - ${data.release_date} \n " +
"Overview: ${data.overview} \n " +
"Vote: ${data.vote_average} \n " +
- "${MovieUrl.BASE_URL_IMAGE_ORIGNAL}${data.poster_path}" +
+ "${MovieUrl.BASE_URL_IMAGE_ORIGINAL}${data.poster_path}" +
"\n" +
"Terima Kasih"
currentInputConnection?.commitText(output, 1)
@@ -131,7 +131,7 @@ class MovieKeyboard(
frogoRvGridType6TvTitle.text = data.title
frogoRvGridType6TvSubtitle.text = data.release_date
frogoRvGridType6TvDesc.text = data.overview
- Glide.with(context).load("${MovieUrl.BASE_URL_IMAGE_ORIGNAL}${data.poster_path}").into(frogoRvGridType6CivPoster)
+ 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/language/KeyboardLanguage.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/language/KeyboardLanguage.kt
index 0afb5ba..9804d69 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/ui/language/KeyboardLanguage.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/language/KeyboardLanguage.kt
@@ -14,4 +14,8 @@ package com.frogobox.appkeyboard.ui.language
data class KeyboardLanguage(
val name: String,
val xml: Int,
+ val layoutType: String = "Standard",
+ val code: String = "EN",
+ val script: String = "Latin",
+ val isRtl: Boolean = false
)
\ No newline at end of file
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 50ae054..f5fc81a 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
@@ -1,135 +1,47 @@
package com.frogobox.appkeyboard.ui.language
import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.ViewGroup
import androidx.activity.viewModels
-import com.frogobox.appkeyboard.common.base.BaseActivity
-import com.frogobox.appkeyboard.databinding.ActivityKeyboardLanguageBinding
-import com.frogobox.appkeyboard.databinding.ItemLanguageKeyboardBinding
-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 androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import com.frogobox.appkeyboard.common.base.BaseComposeActivity
import com.frogobox.sdk.ext.showToast
-import com.frogobox.sdk.ext.visible
-import com.frogobox.sdk.util.FrogoFunc
-import com.frogobox.sdk.util.SimpleDialogUtil
import dagger.hilt.android.AndroidEntryPoint
-/**
- * Created by Faisal Amir on 24/10/22
- * -----------------------------------------
- * E-mail : faisalamircs@gmail.com
- * Github : github.com/amirisback
- * -----------------------------------------
- * Copyright (C) Frogobox ID / amirisback
- * All rights reserved
- */
-
@AndroidEntryPoint
-class KeyboardLanguageActivity : BaseActivity(),
- IFrogoBindingAdapter {
+class KeyboardLanguageActivity : BaseComposeActivity() {
private val viewModel: KeyboardLanguageViewModel by viewModels()
- override fun setupViewBinding(): ActivityKeyboardLanguageBinding {
- return ActivityKeyboardLanguageBinding.inflate(layoutInflater)
- }
-
- override fun setupViewModel() {
- super.setupViewModel()
- viewModel.apply {
- keyboardLanguage.observe(this@KeyboardLanguageActivity) {
- setupRV(it)
- }
- }
- }
+ private var languageList by mutableStateOf>(emptyList())
+ private var activeLanguageXml by mutableStateOf(0)
override fun onCreateExt(savedInstanceState: Bundle?) {
super.onCreateExt(savedInstanceState)
- if (savedInstanceState == null) {
- viewModel.getKeyboardLanguage(this)
- }
- }
-
- override fun initView() {
- super.initView()
- setupDetailActivity("Keyboard Language")
- binding.apply {
-
+ viewModel.keyboardLanguage.observe(this) {
+ languageList = it
+ val active = it.firstOrNull { lang -> viewModel.checkKeyboardType(lang.xml) }
+ activeLanguageXml = active?.xml ?: 0
}
- }
-
- override fun areContentsTheSame(oldItem: KeyboardLanguage, newItem: KeyboardLanguage): Boolean {
- return oldItem == newItem
- }
-
- override fun areItemsTheSame(oldItem: KeyboardLanguage, newItem: KeyboardLanguage): Boolean {
- return oldItem.xml == newItem.xml
- }
-
- override fun setViewBinding(parent: ViewGroup): ItemLanguageKeyboardBinding {
- return ItemLanguageKeyboardBinding.inflate(LayoutInflater.from(parent.context), parent, false)
- }
-
- override fun onItemClicked(
- binding: ItemLanguageKeyboardBinding,
- data: KeyboardLanguage,
- position: Int,
- notifyListener: FrogoRecyclerNotifyListener
- ) {
- applyLanguage(data)
- }
-
- override fun onItemLongClicked(
- binding: ItemLanguageKeyboardBinding,
- data: KeyboardLanguage,
- position: Int,
- notifyListener: FrogoRecyclerNotifyListener
- ) {}
-
- override fun setupInitComponent(
- binding: ItemLanguageKeyboardBinding,
- data: KeyboardLanguage,
- position: Int,
- notifyListener: FrogoRecyclerNotifyListener
- ) {
- binding.apply {
- tvItemKeyboardMain.text = data.name
- if (viewModel.checkKeyboardType(data.xml)) {
- this.ivIcon.visible()
- } else {
- this.ivIcon.gone()
- }
- }
- }
-
- private fun setupRV(data: List) {
- binding.rvKeyboardLanguage
- .injectorBinding()
- .addData(data)
- .addCallback(this)
- .createLayoutLinearVertical(false)
- .build()
- }
-
- private fun applyLanguage(data: KeyboardLanguage) {
- SimpleDialogUtil.create(
- this@KeyboardLanguageActivity,
- "Change Theme",
- "Are you sure you want to change keyboard theme?",
- object : SimpleDialogUtil.OnDialogClickListener {
-
- override fun negativeButton() {}
- override fun positiveButton() {
- viewModel.setKeyboard(data.xml) {
- showToast("${data.name} Language Applied")
- viewModel.getKeyboardLanguage(this@KeyboardLanguageActivity)
- }
+ viewModel.getKeyboardLanguage(this)
+ }
+
+ @Composable
+ override fun Content() {
+ KeyboardLanguageScreen(
+ languageList = languageList,
+ activeLanguageXml = activeLanguageXml,
+ checkIsSelected = { xml -> xml == activeLanguageXml },
+ onApplyLanguage = { language ->
+ viewModel.setKeyboard(language.xml) {
+ activeLanguageXml = language.xml
+ showToast("${language.name} Language Applied")
+ viewModel.getKeyboardLanguage(this@KeyboardLanguageActivity)
}
- }
+ },
+ onBackClick = { finish() }
)
}
-
}
\ No newline at end of file
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
new file mode 100644
index 0000000..2daf85d
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/language/KeyboardLanguageScreen.kt
@@ -0,0 +1,831 @@
+package com.frogobox.appkeyboard.ui.language
+
+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.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.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.CheckCircle
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Language
+import androidx.compose.material.icons.filled.RadioButtonUnchecked
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+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
+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.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
+
+@Composable
+fun KeyboardLanguageScreen(
+ languageList: List,
+ checkIsSelected: (Int) -> Boolean,
+ onApplyLanguage: (KeyboardLanguage) -> Unit,
+ onBackClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ activeLanguageXml: Int = 0
+) {
+ var searchQuery by remember { mutableStateOf("") }
+ var selectedLanguageForDialog by remember { mutableStateOf(null) }
+
+ // Resolve active language object
+ val activeLanguage = remember(activeLanguageXml, languageList) {
+ if (activeLanguageXml != 0) {
+ languageList.firstOrNull { it.xml == activeLanguageXml }
+ } else {
+ languageList.firstOrNull { checkIsSelected(it.xml) }
+ }
+ }
+
+ // Filter languages reactively based on query
+ val filteredLanguages = remember(searchQuery, languageList) {
+ if (searchQuery.isBlank()) {
+ languageList
+ } else {
+ val query = searchQuery.trim().lowercase()
+ languageList.filter {
+ it.name.lowercase().contains(query) ||
+ it.code.lowercase().contains(query) ||
+ it.layoutType.lowercase().contains(query) ||
+ it.script.lowercase().contains(query)
+ }
+ }
+ }
+
+ Scaffold(
+ modifier = modifier.fillMaxSize(),
+ topBar = {
+ FrogoTopAppBar(
+ title = "Keyboard Language",
+ onBackClick = onBackClick
+ )
+ },
+ containerColor = MaterialTheme.colorScheme.background
+ ) { paddingValues ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ // 1. Active Language Hero Card (Only shown when not searching, or when active matches query)
+ if (activeLanguage != null && searchQuery.isBlank()) {
+ item(key = "active_hero_card") {
+ ActiveLanguageHeroCard(
+ language = activeLanguage
+ )
+ }
+ }
+
+ // 2. Search Field & Results Header
+ item(key = "search_header") {
+ SearchAndFilterHeader(
+ query = searchQuery,
+ onQueryChange = { searchQuery = it },
+ totalLanguages = languageList.size,
+ filteredCount = filteredLanguages.size
+ )
+ }
+
+ // 3. Empty Search State
+ if (filteredLanguages.isEmpty()) {
+ item(key = "empty_state") {
+ EmptySearchState(
+ query = searchQuery,
+ onClearSearch = { searchQuery = "" }
+ )
+ }
+ } else {
+ // 4. Filtered / Full Language Cards List
+ items(
+ items = filteredLanguages,
+ key = { it.name + it.xml }
+ ) { language ->
+ val isSelected = if (activeLanguageXml != 0) {
+ language.xml == activeLanguageXml
+ } else {
+ checkIsSelected(language.xml)
+ }
+
+ LanguageCard(
+ language = language,
+ isSelected = isSelected,
+ onClick = {
+ selectedLanguageForDialog = language
+ }
+ )
+ }
+ }
+ }
+ }
+
+ // Confirmation Dialog
+ selectedLanguageForDialog?.let { language ->
+ AlertDialog(
+ onDismissRequest = { selectedLanguageForDialog = null },
+ title = {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(36.dp)
+ .clip(CircleShape)
+ .background(FrogoPrimary.copy(alpha = 0.15f)),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.Language,
+ contentDescription = null,
+ tint = FrogoPrimary,
+ modifier = Modifier.size(20.dp)
+ )
+ }
+ Text(
+ text = "Change Keyboard Layout",
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.Bold
+ )
+ )
+ }
+ },
+ text = {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Text(
+ text = "Are you sure you want to switch your active typing layout?",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ // Selected Language Preview Box
+ Surface(
+ shape = RoundedCornerShape(12.dp),
+ color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .size(40.dp)
+ .clip(CircleShape)
+ .background(FrogoPrimary.copy(alpha = 0.2f)),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = language.code,
+ style = MaterialTheme.typography.titleSmall.copy(
+ fontWeight = FontWeight.Bold,
+ color = FrogoPrimary
+ )
+ )
+ }
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = language.name,
+ style = MaterialTheme.typography.bodyLarge.copy(
+ fontWeight = FontWeight.SemiBold
+ ),
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Text(
+ text = "${language.layoutType} Layout • ${language.script}",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+ }
+ },
+ confirmButton = {
+ Button(
+ onClick = {
+ val current = language
+ selectedLanguageForDialog = null
+ onApplyLanguage(current)
+ },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = FrogoPrimary
+ ),
+ shape = RoundedCornerShape(10.dp)
+ ) {
+ Text(
+ text = "Apply Layout",
+ fontWeight = FontWeight.Bold
+ )
+ }
+ },
+ dismissButton = {
+ TextButton(
+ onClick = { selectedLanguageForDialog = null }
+ ) {
+ Text("Cancel")
+ }
+ },
+ shape = RoundedCornerShape(18.dp)
+ )
+ }
+}
+
+/**
+ * Overload function for backward compatibility and clean call sites.
+ */
+@Composable
+fun KeyboardLanguageScreen(
+ languageList: List,
+ activeLanguageXml: Int,
+ onApplyLanguage: (KeyboardLanguage) -> Unit,
+ onBackClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ KeyboardLanguageScreen(
+ languageList = languageList,
+ checkIsSelected = { it == activeLanguageXml },
+ onApplyLanguage = onApplyLanguage,
+ onBackClick = onBackClick,
+ modifier = modifier,
+ activeLanguageXml = activeLanguageXml
+ )
+}
+
+/**
+ * Prominent Hero Card displaying the currently active keyboard language.
+ */
+@Composable
+private fun ActiveLanguageHeroCard(
+ language: KeyboardLanguage,
+ modifier: Modifier = Modifier
+) {
+ Card(
+ modifier = modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(18.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = FrogoPrimary.copy(alpha = 0.08f)
+ ),
+ border = BorderStroke(1.5.dp, FrogoPrimary.copy(alpha = 0.35f)),
+ elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ // Header: Badge Pill
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Surface(
+ shape = RoundedCornerShape(8.dp),
+ color = FrogoStatusSuccess.copy(alpha = 0.15f)
+ ) {
+ Row(
+ modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(6.dp)
+ .clip(CircleShape)
+ .background(FrogoStatusSuccess)
+ )
+ Text(
+ text = "CURRENT ACTIVE",
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 10.sp,
+ color = FrogoStatusSuccess
+ )
+ )
+ }
+ }
+
+ Text(
+ text = "Default Input Method",
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ // Main Content Row
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // Language Code Avatar
+ Box(
+ modifier = Modifier
+ .size(48.dp)
+ .clip(CircleShape)
+ .background(FrogoPrimary.copy(alpha = 0.15f)),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = language.code,
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.Bold,
+ color = FrogoPrimary,
+ fontSize = 17.sp
+ )
+ )
+ }
+
+ Spacer(modifier = Modifier.width(14.dp))
+
+ // Titles & Subtitles
+ Column(
+ modifier = Modifier.weight(1f),
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = language.name,
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 17.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurface,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ Spacer(modifier = Modifier.height(2.dp))
+
+ Text(
+ text = "${language.layoutType} Layout • ${language.script}",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Icon(
+ imageVector = Icons.Default.CheckCircle,
+ contentDescription = "Active Indicator",
+ tint = FrogoStatusSuccess,
+ modifier = Modifier.size(26.dp)
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Modern Search Field with Result Statistics
+ */
+@Composable
+private fun SearchAndFilterHeader(
+ query: String,
+ onQueryChange: (String) -> Unit,
+ totalLanguages: Int,
+ filteredCount: Int,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ OutlinedTextField(
+ value = query,
+ onValueChange = onQueryChange,
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(16.dp),
+ placeholder = {
+ Text(
+ text = "Search language, code, or layout...",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
+ )
+ },
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.Default.Search,
+ contentDescription = "Search",
+ tint = FrogoPrimary,
+ modifier = Modifier.size(20.dp)
+ )
+ },
+ trailingIcon = {
+ if (query.isNotBlank()) {
+ IconButton(onClick = { onQueryChange("") }) {
+ Icon(
+ imageVector = Icons.Default.Close,
+ contentDescription = "Clear Search",
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.size(20.dp)
+ )
+ }
+ }
+ },
+ singleLine = true,
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = FrogoPrimary,
+ unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f),
+ focusedContainerColor = MaterialTheme.colorScheme.surface,
+ unfocusedContainerColor = MaterialTheme.colorScheme.surface
+ )
+ )
+
+ // Count / Section Label
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 4.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = if (query.isBlank()) "Available Languages" else "Search Results",
+ style = MaterialTheme.typography.titleSmall.copy(
+ fontWeight = FontWeight.Bold
+ ),
+ color = MaterialTheme.colorScheme.onSurface
+ )
+
+ Surface(
+ shape = RoundedCornerShape(12.dp),
+ color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
+ ) {
+ Text(
+ text = if (query.isBlank()) "$totalLanguages available" else "$filteredCount found",
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.Medium
+ ),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp)
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Individual Language Item Card with Avatar Badge, Subtitles, and Selection State.
+ */
+@Composable
+private fun LanguageCard(
+ language: KeyboardLanguage,
+ isSelected: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Card(
+ modifier = modifier
+ .fillMaxWidth()
+ .clickable { onClick() },
+ shape = RoundedCornerShape(16.dp),
+ elevation = CardDefaults.cardElevation(
+ defaultElevation = if (isSelected) 3.dp else 1.dp
+ ),
+ colors = CardDefaults.cardColors(
+ containerColor = if (isSelected) {
+ FrogoPrimary.copy(alpha = 0.04f)
+ } else {
+ MaterialTheme.colorScheme.surface
+ }
+ ),
+ border = BorderStroke(
+ width = if (isSelected) 2.dp else 1.dp,
+ color = if (isSelected) {
+ FrogoPrimary
+ } else {
+ MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.45f)
+ }
+ )
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(14.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // Circular Avatar Container with Language Code
+ Box(
+ modifier = Modifier
+ .size(44.dp)
+ .clip(CircleShape)
+ .background(
+ if (isSelected) FrogoPrimary.copy(alpha = 0.18f)
+ else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f)
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = language.code,
+ style = MaterialTheme.typography.titleSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 15.sp,
+ color = if (isSelected) FrogoPrimary else MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ )
+ }
+
+ Spacer(modifier = Modifier.width(14.dp))
+
+ // Text Info Column
+ Column(
+ modifier = Modifier.weight(1f),
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ Text(
+ text = language.name,
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 15.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurface,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ // Layout Badge Chip
+ Surface(
+ shape = RoundedCornerShape(6.dp),
+ color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f)
+ ) {
+ Text(
+ text = language.layoutType,
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 10.sp,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ ),
+ modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
+ )
+ }
+
+ // Script details
+ Text(
+ text = "• ${language.script}",
+ style = MaterialTheme.typography.bodySmall.copy(
+ fontSize = 11.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ // RTL Tag if applicable
+ if (language.isRtl) {
+ Surface(
+ shape = RoundedCornerShape(6.dp),
+ color = FrogoPrimary.copy(alpha = 0.1f)
+ ) {
+ Text(
+ text = "RTL",
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 9.sp,
+ color = FrogoPrimary
+ ),
+ modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp)
+ )
+ }
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ // Trailing Status Indicator
+ if (isSelected) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ Surface(
+ shape = RoundedCornerShape(8.dp),
+ color = FrogoStatusSuccess.copy(alpha = 0.15f)
+ ) {
+ Text(
+ text = "ACTIVE",
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 10.sp,
+ color = FrogoStatusSuccess
+ ),
+ modifier = Modifier.padding(horizontal = 6.dp, vertical = 3.dp)
+ )
+ }
+
+ Icon(
+ imageVector = Icons.Default.CheckCircle,
+ contentDescription = "Active Language",
+ tint = FrogoStatusSuccess,
+ modifier = Modifier.size(22.dp)
+ )
+ }
+ } else {
+ Icon(
+ imageVector = Icons.Default.RadioButtonUnchecked,
+ contentDescription = "Select Language",
+ tint = MaterialTheme.colorScheme.outlineVariant,
+ modifier = Modifier.size(20.dp)
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Empty State when search query yields no matches.
+ */
+@Composable
+private fun EmptySearchState(
+ query: String,
+ onClearSearch: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(vertical = 40.dp, horizontal = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Box(
+ modifier = Modifier
+ .size(64.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.Search,
+ contentDescription = null,
+ modifier = Modifier.size(32.dp),
+ tint = MaterialTheme.colorScheme.outline
+ )
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ text = "No Language Found",
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.Bold
+ ),
+ color = MaterialTheme.colorScheme.onSurface,
+ textAlign = TextAlign.Center
+ )
+
+ Spacer(modifier = Modifier.height(6.dp))
+
+ Text(
+ text = "We couldn't find any language or layout matching \"$query\".\nTry searching by code or layout type.",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ textAlign = TextAlign.Center
+ )
+
+ Spacer(modifier = Modifier.height(18.dp))
+
+ TextButton(
+ onClick = onClearSearch,
+ colors = ButtonDefaults.textButtonColors(
+ contentColor = FrogoPrimary
+ )
+ ) {
+ Text(
+ text = "Reset Search",
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun KeyboardLanguageScreenPreview() {
+ FrogoKeyboardTheme(darkTheme = false) {
+ KeyboardLanguageScreen(
+ languageList = listOf(
+ KeyboardLanguage(
+ name = "English (QWERTY)",
+ xml = 1,
+ layoutType = "QWERTY",
+ code = "EN",
+ script = "Latin"
+ ),
+ KeyboardLanguage(
+ name = "German",
+ xml = 2,
+ layoutType = "QWERTZ",
+ code = "DE",
+ script = "Deutsch (Latin)"
+ ),
+ KeyboardLanguage(
+ name = "French",
+ xml = 3,
+ layoutType = "AZERTY",
+ code = "FR",
+ script = "Français (Latin)"
+ ),
+ KeyboardLanguage(
+ name = "Persian",
+ xml = 4,
+ layoutType = "Standard",
+ code = "FA",
+ script = "فارسی (Perso-Arabic)",
+ isRtl = true
+ )
+ ),
+ checkIsSelected = { it == 1 },
+ onApplyLanguage = {},
+ onBackClick = {},
+ activeLanguageXml = 1
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun KeyboardLanguageScreenDarkPreview() {
+ FrogoKeyboardTheme(darkTheme = true) {
+ KeyboardLanguageScreen(
+ languageList = listOf(
+ KeyboardLanguage(
+ name = "English (QWERTY)",
+ xml = 1,
+ layoutType = "QWERTY",
+ code = "EN",
+ script = "Latin"
+ ),
+ KeyboardLanguage(
+ name = "Russian",
+ xml = 2,
+ layoutType = "ЙЦУКЕН",
+ code = "RU",
+ script = "Русский (Cyrillic)"
+ )
+ ),
+ checkIsSelected = { it == 1 },
+ onApplyLanguage = {},
+ onBackClick = {},
+ activeLanguageXml = 1
+ )
+ }
+}
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 3158b85..a096b1a 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
@@ -36,88 +36,149 @@ class KeyboardLanguageViewModel @Inject constructor(
val list = mutableListOf()
list.add(
KeyboardLanguage(
- "${context.getString(com.frogobox.libkeyboard.R.string.translation_english)} (QWERTY)",
- com.frogobox.libkeyboard.R.xml.keys_letters_qwerty
+ name = "${context.getString(com.frogobox.libkeyboard.R.string.translation_english)} (QWERTY)",
+ xml = com.frogobox.libkeyboard.R.xml.keys_letters_qwerty,
+ layoutType = "QWERTY",
+ code = "EN",
+ script = "Latin"
)
)
list.add(
KeyboardLanguage(
- "${context.getString(com.frogobox.libkeyboard.R.string.translation_english)} (QWERTZ)",
- com.frogobox.libkeyboard.R.xml.keys_letters_english_qwertz
+ name = "Programmer / Coding (QWERTY)",
+ xml = com.frogobox.libkeyboard.R.xml.keys_letters_programmer,
+ layoutType = "Codeboard",
+ code = "DEV",
+ script = "Code & Symbols"
)
)
list.add(
KeyboardLanguage(
- "${context.getString(com.frogobox.libkeyboard.R.string.translation_english)} (DVORAK)",
- com.frogobox.libkeyboard.R.xml.keys_letters_english_dvorak
+ name = "${context.getString(com.frogobox.libkeyboard.R.string.translation_english)} (QWERTZ)",
+ xml = com.frogobox.libkeyboard.R.xml.keys_letters_english_qwertz,
+ layoutType = "QWERTZ",
+ code = "EN",
+ script = "Latin"
+ )
+ )
+ list.add(
+ KeyboardLanguage(
+ name = "${context.getString(com.frogobox.libkeyboard.R.string.translation_english)} (DVORAK)",
+ xml = com.frogobox.libkeyboard.R.xml.keys_letters_english_dvorak,
+ layoutType = "DVORAK",
+ code = "EN",
+ script = "Latin"
)
)
list.add(
KeyboardLanguage(
- context.getString(com.frogobox.libkeyboard.R.string.translation_bengali),
- com.frogobox.libkeyboard.R.xml.keys_letters_bengali
+ name = context.getString(com.frogobox.libkeyboard.R.string.translation_bengali),
+ xml = com.frogobox.libkeyboard.R.xml.keys_letters_bengali,
+ layoutType = "Standard",
+ code = "BN",
+ script = "বাংলা (Bengali)"
)
)
list.add(
KeyboardLanguage(
- context.getString(com.frogobox.libkeyboard.R.string.translation_bulgarian),
- com.frogobox.libkeyboard.R.xml.keys_letters_bulgarian
+ name = context.getString(com.frogobox.libkeyboard.R.string.translation_bulgarian),
+ xml = com.frogobox.libkeyboard.R.xml.keys_letters_bulgarian,
+ layoutType = "Standard",
+ code = "BG",
+ script = "Български (Cyrillic)"
)
)
list.add(
KeyboardLanguage(
- context.getString(com.frogobox.libkeyboard.R.string.translation_french),
- com.frogobox.libkeyboard.R.xml.keys_letters_french
+ name = context.getString(com.frogobox.libkeyboard.R.string.translation_french),
+ xml = com.frogobox.libkeyboard.R.xml.keys_letters_french,
+ layoutType = "AZERTY",
+ code = "FR",
+ script = "Français (Latin)"
+ )
+ )
+ list.add(
+ KeyboardLanguage(
+ name = context.getString(com.frogobox.libkeyboard.R.string.translation_german),
+ xml = com.frogobox.libkeyboard.R.xml.keys_letters_german,
+ layoutType = "QWERTZ",
+ code = "DE",
+ script = "Deutsch (Latin)"
)
)
list.add(
KeyboardLanguage(
- context.getString(com.frogobox.libkeyboard.R.string.translation_german),
- com.frogobox.libkeyboard.R.xml.keys_letters_german
+ name = context.getString(com.frogobox.libkeyboard.R.string.translation_greek),
+ xml = com.frogobox.libkeyboard.R.xml.keys_letters_greek,
+ layoutType = "Standard",
+ code = "EL",
+ script = "Ελληνικά (Greek)"
)
)
list.add(
KeyboardLanguage(
- context.getString(com.frogobox.libkeyboard.R.string.translation_greek),
- com.frogobox.libkeyboard.R.xml.keys_letters_greek
+ name = context.getString(com.frogobox.libkeyboard.R.string.translation_lithuanian),
+ xml = com.frogobox.libkeyboard.R.xml.keys_letters_lithuanian,
+ layoutType = "ĄŽERTY",
+ code = "LT",
+ script = "Lietuvių (Baltic)"
)
)
list.add(
KeyboardLanguage(
- context.getString(com.frogobox.libkeyboard.R.string.translation_lithuanian),
- com.frogobox.libkeyboard.R.xml.keys_letters_lithuanian
+ name = context.getString(com.frogobox.libkeyboard.R.string.translation_romanian),
+ xml = com.frogobox.libkeyboard.R.xml.keys_letters_romanian,
+ layoutType = "QWERTY",
+ code = "RO",
+ script = "Română (Latin)"
)
)
list.add(
KeyboardLanguage(
- context.getString(com.frogobox.libkeyboard.R.string.translation_romanian),
- com.frogobox.libkeyboard.R.xml.keys_letters_romanian
+ name = context.getString(com.frogobox.libkeyboard.R.string.translation_russian),
+ xml = com.frogobox.libkeyboard.R.xml.keys_letters_russian,
+ layoutType = "ЙЦУКЕН",
+ code = "RU",
+ script = "Русский (Cyrillic)"
)
)
list.add(
KeyboardLanguage(
- context.getString(com.frogobox.libkeyboard.R.string.translation_slovenian),
- com.frogobox.libkeyboard.R.xml.keys_letters_slovenian
+ name = context.getString(com.frogobox.libkeyboard.R.string.translation_slovenian),
+ xml = com.frogobox.libkeyboard.R.xml.keys_letters_slovenian,
+ layoutType = "QWERTZ",
+ code = "SL",
+ script = "Slovenščina (Latin)"
)
)
list.add(
KeyboardLanguage(
- context.getString(com.frogobox.libkeyboard.R.string.translation_spanish),
- com.frogobox.libkeyboard.R.xml.keys_letters_spanish_qwerty
+ name = context.getString(com.frogobox.libkeyboard.R.string.translation_spanish),
+ xml = com.frogobox.libkeyboard.R.xml.keys_letters_spanish_qwerty,
+ layoutType = "QWERTY",
+ code = "ES",
+ script = "Español (Latin)"
)
)
list.add(
KeyboardLanguage(
- "${context.getString(com.frogobox.libkeyboard.R.string.translation_turkish)} (Q)",
- com.frogobox.libkeyboard.R.xml.keys_letters_turkish_q
+ name = "${context.getString(com.frogobox.libkeyboard.R.string.translation_turkish)} (Q)",
+ xml = com.frogobox.libkeyboard.R.xml.keys_letters_turkish_q,
+ layoutType = "QWERTY",
+ code = "TR",
+ script = "Türkçe (Latin)"
)
)
list.add(
KeyboardLanguage(
- context.getString(com.frogobox.libkeyboard.R.string.translation_persian),
- com.frogobox.libkeyboard.R.xml.keys_letter_persian
+ name = context.getString(com.frogobox.libkeyboard.R.string.translation_persian),
+ xml = com.frogobox.libkeyboard.R.xml.keys_letter_persian,
+ layoutType = "Standard",
+ code = "FA",
+ script = "فارسی (Perso-Arabic)",
+ isRtl = true
)
)
_keyboardLanguage.postValue(list)
@@ -137,4 +198,11 @@ class KeyboardLanguageViewModel @Inject constructor(
) == xml
}
+ fun getActiveKeyboardXml(): Int {
+ return pref.getPrefInt(
+ KeyboardUtil.KEYBOARD_TYPE,
+ com.frogobox.libkeyboard.R.xml.keys_letters_qwerty
+ )
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/main/MainActivity.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/main/MainActivity.kt
index ea37ace..41ad94b 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/ui/main/MainActivity.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/main/MainActivity.kt
@@ -3,106 +3,50 @@ package com.frogobox.appkeyboard.ui.main
import android.content.Intent
import android.os.Bundle
import android.provider.Settings
-import android.util.Log
import android.view.inputmethod.InputMethodManager
import androidx.activity.viewModels
-import com.frogobox.appkeyboard.databinding.ActivityMainBinding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import com.frogobox.appkeyboard.common.base.BaseComposeMainActivity
import com.frogobox.appkeyboard.ui.autotext.AutoTextActivity
import com.frogobox.appkeyboard.ui.language.KeyboardLanguageActivity
+import com.frogobox.appkeyboard.ui.sound.SoundActivity
import com.frogobox.appkeyboard.ui.test.TestActivity
import com.frogobox.appkeyboard.ui.theme.ThemeActivity
import com.frogobox.appkeyboard.ui.toggle.ToggleActivity
-import com.frogobox.sdk.ext.getColorExt
import com.frogobox.sdk.ext.startActivityExt
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
-class MainActivity : BaseMainActivity() {
+class MainActivity : BaseComposeMainActivity() {
+
+ private val viewModel: MainViewModel by viewModels()
private val NONE = 0
private val PICKING = 1
private val CHOSEN = 2
-
private var mState = 0
- companion object {
- private val TAG: String = MainActivity::class.java.simpleName
- }
-
- private val viewModel: MainViewModel by viewModels()
-
- override fun setupViewBinding(): ActivityMainBinding {
- return ActivityMainBinding.inflate(layoutInflater)
- }
-
- override fun setupViewModel() {
- super.setupViewModel()
- viewModel.apply {
-
- }
- }
+ private var keyboardStatus by mutableStateOf(KeyboardStatus.NOT_ENABLED)
override fun onCreateExt(savedInstanceState: Bundle?) {
super.onCreateExt(savedInstanceState)
- if (savedInstanceState == null) {
- // Call View Model Here
- Log.d(TAG, "View Model : ${viewModel::class.java.simpleName}")
- }
- // TODO : Add your code here
- initView()
- }
-
- override fun onWindowFocusChanged(hasFocus: Boolean) {
- super.onWindowFocusChanged(hasFocus)
- if (mState === PICKING) {
- mState = CHOSEN
- } else if (mState === CHOSEN) {
- handlingState()
- }
+ updateKeyboardStatus()
}
override fun onResume() {
super.onResume()
- handlingState()
+ updateKeyboardStatus()
}
- override fun initView() {
- super.initView()
- binding.apply {
- handlingState()
-
- btnChangeKeyboard.setOnClickListener {
- (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager).showInputMethodPicker()
- mState = PICKING
- }
-
- btnGoToSetting.setOnClickListener {
- Intent(Settings.ACTION_INPUT_METHOD_SETTINGS).apply {
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- startActivity(this)
- }
- }
-
- btnDoSomeTest.setOnClickListener {
- startActivityExt()
- }
-
- btnAutoText.setOnClickListener {
- startActivityExt()
- }
-
- btnToggle.setOnClickListener {
- startActivityExt()
- }
-
- btnMultiLanguage.setOnClickListener {
- startActivityExt()
- }
-
- btnTheme.setOnClickListener {
- startActivityExt()
- }
-
+ override fun onWindowFocusChanged(hasFocus: Boolean) {
+ super.onWindowFocusChanged(hasFocus)
+ if (mState == PICKING) {
+ mState = CHOSEN
+ } else if (mState == CHOSEN) {
+ updateKeyboardStatus()
}
}
@@ -120,21 +64,34 @@ class MainActivity : BaseMainActivity() {
}
}
- private fun handlingState() {
- binding.titleState.apply {
- if (!isKeyboardEnabled()) {
- text = "Frogo Keyboard Not Active"
- setTextColor(getColorExt(com.frogobox.libkeyboard.R.color.status_failed))
- } else {
- if (isUsingKeyboard()) {
- text = "Frogo Keyboard Active"
- setTextColor(getColorExt(com.frogobox.libkeyboard.R.color.status_success))
- } else {
- text = "Not Using Frogo Keyboard"
- setTextColor(getColorExt(com.frogobox.libkeyboard.R.color.status_warning))
- }
- }
+ private fun updateKeyboardStatus() {
+ keyboardStatus = when {
+ !isKeyboardEnabled() -> KeyboardStatus.NOT_ENABLED
+ isUsingKeyboard() -> KeyboardStatus.ACTIVE
+ else -> KeyboardStatus.NOT_DEFAULT
}
}
+ @Composable
+ override fun Content() {
+ MainScreen(
+ status = keyboardStatus,
+ onGoToSettings = {
+ Intent(Settings.ACTION_INPUT_METHOD_SETTINGS).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(this)
+ }
+ },
+ onChangeKeyboard = {
+ (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager).showInputMethodPicker()
+ mState = PICKING
+ },
+ onNavigateAutoText = { startActivityExt() },
+ onNavigateToggle = { startActivityExt() },
+ onNavigateLanguage = { startActivityExt() },
+ onNavigateTheme = { startActivityExt() },
+ onNavigateSound = { startActivityExt() },
+ onNavigateTest = { startActivityExt() }
+ )
+ }
}
\ No newline at end of file
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
new file mode 100644
index 0000000..3e18982
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/main/MainScreen.kt
@@ -0,0 +1,473 @@
+package com.frogobox.appkeyboard.ui.main
+
+import androidx.compose.foundation.BorderStroke
+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.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.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
+import androidx.compose.material.icons.filled.CheckCircle
+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.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+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.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.painterResource
+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 com.frogobox.appkeyboard.R
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoKeyboardTheme
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoPrimary
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoStatusFailed
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoStatusSuccess
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoStatusWarning
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoTopAppBar
+
+enum class KeyboardStatus(
+ val title: String,
+ val description: String,
+ val badge: String,
+ val color: Color,
+ val icon: ImageVector
+) {
+ ACTIVE(
+ title = "Frogo Keyboard Active",
+ description = "Keyboard is activated and set as default input method.",
+ badge = "ACTIVE",
+ 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",
+ 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",
+ color = FrogoStatusFailed,
+ icon = Icons.Default.Info
+ )
+}
+
+@Composable
+fun MainScreen(
+ status: KeyboardStatus,
+ onGoToSettings: () -> Unit,
+ onChangeKeyboard: () -> Unit,
+ onNavigateAutoText: () -> Unit,
+ onNavigateToggle: () -> Unit,
+ onNavigateLanguage: () -> Unit,
+ onNavigateTheme: () -> Unit,
+ onNavigateSound: () -> Unit,
+ onNavigateTest: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val scrollState = rememberScrollState()
+
+ Scaffold(
+ modifier = modifier.fillMaxSize(),
+ topBar = {
+ FrogoTopAppBar(
+ title = "Frogo Keyboard"
+ )
+ },
+ containerColor = MaterialTheme.colorScheme.background
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(scrollState)
+ .padding(paddingValues)
+ .padding(horizontal = 20.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.",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // Reactive Status Card
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = status.color.copy(alpha = 0.12f)
+ ),
+ border = BorderStroke(1.dp, status.color.copy(alpha = 0.35f)),
+ elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .size(42.dp)
+ .clip(CircleShape)
+ .background(status.color.copy(alpha = 0.2f)),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = status.icon,
+ contentDescription = null,
+ tint = status.color,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+
+ Spacer(modifier = Modifier.width(14.dp))
+
+ Column(modifier = Modifier.weight(1f)) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(
+ text = status.title,
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 15.sp
+ ),
+ color = status.color
+ )
+
+ Surface(
+ shape = RoundedCornerShape(6.dp),
+ color = status.color.copy(alpha = 0.2f)
+ ) {
+ Text(
+ text = status.badge,
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 10.sp
+ ),
+ color = status.color,
+ modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ Text(
+ text = status.description,
+ style = MaterialTheme.typography.bodySmall.copy(
+ fontSize = 12.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // Activation Steps Row (Step 1: Settings, Step 2: Switch Keyboard)
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ ActivationCard(
+ stepNumber = "1",
+ title = "Enable Keyboard",
+ subtitle = "System Settings",
+ iconRes = R.drawable.ic_menu_setting,
+ onClick = onGoToSettings,
+ modifier = Modifier.weight(1f)
+ )
+
+ ActivationCard(
+ stepNumber = "2",
+ title = "Set Default",
+ subtitle = "Switch Keyboard",
+ iconRes = R.drawable.ic_menu_keyboard,
+ onClick = onChangeKeyboard,
+ modifier = Modifier.weight(1f)
+ )
+ }
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ // Section Header
+ Text(
+ text = "Features & Customization",
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 16.sp
+ ),
+ color = MaterialTheme.colorScheme.onBackground
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ // Feature Menu List (Material 3 Cards with Icon, Subtitle, and Chevron)
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ MainMenuItemCard(
+ title = "Auto Text",
+ subtitle = "Quick snippets & repetitive text templates",
+ iconRes = R.drawable.ic_menu_auto_text,
+ onClick = onNavigateAutoText
+ )
+
+ MainMenuItemCard(
+ title = "Toggle Function",
+ subtitle = "Keyboard shortcuts & toolbar switches",
+ iconRes = R.drawable.ic_menu_form,
+ onClick = onNavigateToggle
+ )
+
+ MainMenuItemCard(
+ title = "Sound & Haptics",
+ subtitle = "Mechanical switch sounds & key vibration",
+ iconRes = R.drawable.ic_menu_sound,
+ onClick = onNavigateSound
+ )
+
+ MainMenuItemCard(
+ title = "Keyboard Language",
+ subtitle = "Multilingual input languages & layouts",
+ iconRes = R.drawable.ic_menu_website,
+ onClick = onNavigateLanguage
+ )
+
+ MainMenuItemCard(
+ title = "Keyboard Theme",
+ subtitle = "Personalized colors, themes & wallpapers",
+ iconRes = R.drawable.ic_menu_ps_game,
+ onClick = onNavigateTheme
+ )
+
+ MainMenuItemCard(
+ title = "Do Some Test",
+ subtitle = "Interactive typing test & keyboard sandbox",
+ iconRes = R.drawable.ic_menu_keyboard,
+ onClick = onNavigateTest
+ )
+ }
+
+ Spacer(modifier = Modifier.height(24.dp))
+ }
+ }
+}
+
+@Composable
+private fun ActivationCard(
+ stepNumber: String,
+ title: String,
+ subtitle: String,
+ iconRes: Int,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Card(
+ modifier = modifier
+ .clickable(onClick = onClick),
+ shape = RoundedCornerShape(16.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ ),
+ border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.Start
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ painter = painterResource(id = iconRes),
+ contentDescription = title,
+ modifier = Modifier.size(36.dp)
+ )
+
+ Surface(
+ shape = RoundedCornerShape(6.dp),
+ color = FrogoPrimary.copy(alpha = 0.1f)
+ ) {
+ Text(
+ text = "STEP $stepNumber",
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 10.sp
+ ),
+ color = FrogoPrimary,
+ modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Text(
+ text = title,
+ style = MaterialTheme.typography.labelLarge.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 14.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurface
+ )
+
+ Spacer(modifier = Modifier.height(2.dp))
+
+ Text(
+ text = subtitle,
+ style = MaterialTheme.typography.bodySmall.copy(
+ fontSize = 12.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+}
+
+@Composable
+private fun MainMenuItemCard(
+ title: String,
+ subtitle: String,
+ iconRes: Int,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Card(
+ modifier = modifier
+ .fillMaxWidth()
+ .clickable(onClick = onClick),
+ shape = RoundedCornerShape(16.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)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 14.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ painter = painterResource(id = iconRes),
+ contentDescription = title,
+ modifier = Modifier.size(36.dp)
+ )
+
+ Spacer(modifier = Modifier.width(14.dp))
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 15.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurface
+ )
+
+ Spacer(modifier = Modifier.height(2.dp))
+
+ Text(
+ text = subtitle,
+ style = MaterialTheme.typography.bodySmall.copy(
+ fontSize = 12.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
+ modifier = Modifier.size(20.dp)
+ )
+ }
+ }
+}
+
+@Preview(name = "Light Mode - Active", showBackground = true)
+@Composable
+fun MainScreenActivePreview() {
+ FrogoKeyboardTheme(darkTheme = false) {
+ MainScreen(
+ status = KeyboardStatus.ACTIVE,
+ onGoToSettings = {},
+ onChangeKeyboard = {},
+ onNavigateAutoText = {},
+ onNavigateToggle = {},
+ onNavigateLanguage = {},
+ onNavigateTheme = {},
+ onNavigateSound = {},
+ onNavigateTest = {}
+ )
+ }
+}
+
+@Preview(name = "Dark Mode - Inactive", showBackground = true)
+@Composable
+fun MainScreenInactiveDarkPreview() {
+ FrogoKeyboardTheme(darkTheme = true) {
+ MainScreen(
+ status = KeyboardStatus.NOT_ENABLED,
+ onGoToSettings = {},
+ onChangeKeyboard = {},
+ onNavigateAutoText = {},
+ onNavigateToggle = {},
+ onNavigateLanguage = {},
+ onNavigateTheme = {},
+ onNavigateSound = {},
+ onNavigateTest = {}
+ )
+ }
+}
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
new file mode 100644
index 0000000..62e609b
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/sound/SoundActivity.kt
@@ -0,0 +1,47 @@
+package com.frogobox.appkeyboard.ui.sound
+
+import android.os.Bundle
+import androidx.activity.viewModels
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import com.frogobox.appkeyboard.common.base.BaseComposeActivity
+import com.frogobox.libkeyboard.common.sound.MechanicalSoundType
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class SoundActivity : BaseComposeActivity() {
+
+ private val viewModel: SoundViewModel by viewModels()
+
+ private var soundEnabled by mutableStateOf(true)
+ private var selectedSoundType by mutableStateOf(MechanicalSoundType.CHERRY_MX_BLUE)
+ private var soundVolume by mutableStateOf(80)
+ private var vibrateEnabled by mutableStateOf(true)
+
+ override fun onCreateExt(savedInstanceState: Bundle?) {
+ super.onCreateExt(savedInstanceState)
+ viewModel.soundEnabled.observe(this) { soundEnabled = it }
+ viewModel.selectedSoundType.observe(this) { selectedSoundType = it }
+ viewModel.soundVolume.observe(this) { soundVolume = it }
+ viewModel.vibrateEnabled.observe(this) { vibrateEnabled = it }
+ viewModel.loadSettings()
+ }
+
+ @Composable
+ override fun Content() {
+ SoundScreen(
+ soundEnabled = soundEnabled,
+ selectedSoundType = selectedSoundType,
+ soundVolume = soundVolume,
+ vibrateEnabled = vibrateEnabled,
+ onSoundEnabledChange = { viewModel.setSoundEnabled(it) },
+ onSoundTypeSelected = { viewModel.setSoundType(it) },
+ onVolumeChange = { viewModel.setSoundVolume(it) },
+ onVibrateChange = { viewModel.setVibrateEnabled(it) },
+ onTestKeyTap = { viewModel.playTestKeySound() },
+ onBackClick = { finish() }
+ )
+ }
+}
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
new file mode 100644
index 0000000..75b2b8e
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/sound/SoundScreen.kt
@@ -0,0 +1,615 @@
+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
+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.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.CheckCircle
+import androidx.compose.material.icons.filled.Notifications
+import androidx.compose.material.icons.filled.PhoneAndroid
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Slider
+import androidx.compose.material3.SliderDefaults
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Switch
+import androidx.compose.material3.SwitchDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+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
+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.ui.theme.compose.FrogoPrimary
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoStatusSuccess
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoTopAppBar
+import com.frogobox.libkeyboard.common.sound.MechanicalSoundType
+
+@Composable
+fun SoundScreen(
+ soundEnabled: Boolean,
+ selectedSoundType: MechanicalSoundType,
+ soundVolume: Int,
+ vibrateEnabled: Boolean,
+ onSoundEnabledChange: (Boolean) -> Unit,
+ onSoundTypeSelected: (MechanicalSoundType) -> Unit,
+ onVolumeChange: (Int) -> Unit,
+ onVibrateChange: (Boolean) -> Unit,
+ onTestKeyTap: () -> Unit,
+ onBackClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val view = LocalView.current
+
+ Scaffold(
+ modifier = modifier.fillMaxSize(),
+ topBar = {
+ FrogoTopAppBar(
+ title = "Keyboard Sound & Haptics",
+ onBackClick = onBackClick
+ )
+ },
+ containerColor = MaterialTheme.colorScheme.background
+ ) { paddingValues ->
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(2),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 12.dp, bottom = 28.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ // Master Sound Feedback Switch Card
+ item(span = { GridItemSpan(maxLineSpan) }) {
+ MasterSoundCard(
+ soundEnabled = soundEnabled,
+ selectedSoundType = selectedSoundType,
+ onSoundEnabledChange = onSoundEnabledChange
+ )
+ }
+
+ // Section Header: Switch Profiles
+ 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(
+ fontWeight = FontWeight.Bold,
+ fontSize = 12.sp,
+ letterSpacing = 0.8.sp
+ ),
+ color = MaterialTheme.colorScheme.primary
+ )
+ Spacer(modifier = Modifier.height(2.dp))
+ Text(
+ text = "Authentic tactile & acoustic mechanical switch models",
+ style = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+
+ // Grid of Switch Profiles
+ items(MechanicalSoundType.entries.toList(), key = { it.id }) { type ->
+ SoundProfileCard(
+ profile = type,
+ isSelected = selectedSoundType == type,
+ onClick = {
+ onSoundTypeSelected(type)
+ if (vibrateEnabled) {
+ view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
+ }
+ }
+ )
+ }
+
+ // Section Header: Volume & Haptics
+ item(span = { GridItemSpan(maxLineSpan) }) {
+ Column(modifier = Modifier.padding(top = 10.dp, bottom = 2.dp)) {
+ Text(
+ text = "VOLUME & HAPTIC FEEDBACK",
+ style = MaterialTheme.typography.labelMedium.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 12.sp,
+ letterSpacing = 0.8.sp
+ ),
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
+ }
+
+ // Volume Card
+ item(span = { GridItemSpan(maxLineSpan) }) {
+ VolumeControlCard(
+ volume = soundVolume,
+ soundEnabled = soundEnabled,
+ onVolumeChange = onVolumeChange
+ )
+ }
+
+ // Haptic Vibration Card
+ item(span = { GridItemSpan(maxLineSpan) }) {
+ HapticVibrationCard(
+ vibrateEnabled = vibrateEnabled,
+ onVibrateChange = { enabled ->
+ onVibrateChange(enabled)
+ if (enabled) {
+ view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
+ }
+ }
+ )
+ }
+
+ // Interactive Mechanical Sound Playground
+ item(span = { GridItemSpan(maxLineSpan) }) {
+ LiveSoundPlaygroundCard(
+ selectedSoundType = selectedSoundType,
+ soundEnabled = soundEnabled,
+ onKeyTap = {
+ onTestKeyTap()
+ if (vibrateEnabled) {
+ view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
+ }
+ }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun MasterSoundCard(
+ soundEnabled: Boolean,
+ selectedSoundType: MechanicalSoundType,
+ onSoundEnabledChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val primaryColor = MaterialTheme.colorScheme.primary
+ val cardBackground by animateColorAsState(
+ targetValue = if (soundEnabled) primaryColor.copy(alpha = 0.08f) else MaterialTheme.colorScheme.surface,
+ label = "cardBg"
+ )
+
+ Card(
+ modifier = modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(18.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)
+ ),
+ elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .size(46.dp)
+ .clip(CircleShape)
+ .background(
+ if (soundEnabled) FrogoPrimary.copy(alpha = 0.15f)
+ else MaterialTheme.colorScheme.surfaceVariant
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.Notifications,
+ contentDescription = null,
+ tint = if (soundEnabled) FrogoPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+
+ Spacer(modifier = Modifier.width(14.dp))
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = "Mechanical Sound Feedback",
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 15.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Spacer(modifier = Modifier.height(2.dp))
+ Text(
+ text = if (soundEnabled) "Active: ${selectedSoundType.title}" else "Acoustic feedback muted",
+ style = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ Switch(
+ checked = soundEnabled,
+ onCheckedChange = onSoundEnabledChange,
+ colors = SwitchDefaults.colors(
+ checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
+ checkedTrackColor = FrogoPrimary
+ )
+ )
+ }
+ }
+}
+
+@Composable
+private fun SoundProfileCard(
+ profile: MechanicalSoundType,
+ isSelected: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val primaryColor = MaterialTheme.colorScheme.primary
+ val borderColor by animateColorAsState(
+ targetValue = if (isSelected) primaryColor else MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f),
+ label = "border"
+ )
+ val containerColor by animateColorAsState(
+ targetValue = if (isSelected) primaryColor.copy(alpha = 0.08f) else MaterialTheme.colorScheme.surface,
+ label = "container"
+ )
+
+ Card(
+ modifier = modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(16.dp))
+ .clickable(onClick = onClick),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(containerColor = containerColor),
+ border = BorderStroke(if (isSelected) 1.5.dp else 1.dp, borderColor),
+ elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(14.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Surface(
+ shape = RoundedCornerShape(8.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"
+ },
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 9.sp
+ ),
+ color = if (isSelected) FrogoPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
+ )
+ }
+
+ if (isSelected) {
+ Icon(
+ imageVector = Icons.Default.CheckCircle,
+ contentDescription = "Selected",
+ tint = FrogoStatusSuccess,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+
+ Text(
+ text = profile.title,
+ style = MaterialTheme.typography.titleSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 14.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurface,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ Text(
+ text = profile.description,
+ style = MaterialTheme.typography.bodySmall.copy(fontSize = 11.sp),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+}
+
+@Composable
+private fun VolumeControlCard(
+ volume: Int,
+ soundEnabled: Boolean,
+ onVolumeChange: (Int) -> 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.outlineVariant.copy(alpha = 0.35f)),
+ elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = "Acoustic Volume",
+ style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold),
+ color = MaterialTheme.colorScheme.onSurface
+ )
+
+ Text(
+ text = if (soundEnabled) "$volume%" else "Muted",
+ style = MaterialTheme.typography.labelMedium.copy(
+ fontWeight = FontWeight.Bold,
+ color = FrogoPrimary
+ )
+ )
+ }
+
+ Spacer(modifier = Modifier.height(6.dp))
+
+ Slider(
+ value = volume.toFloat(),
+ onValueChange = { onVolumeChange(it.toInt()) },
+ valueRange = 5f..100f,
+ enabled = soundEnabled,
+ colors = SliderDefaults.colors(
+ thumbColor = FrogoPrimary,
+ activeTrackColor = FrogoPrimary,
+ inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant
+ )
+ )
+ }
+ }
+}
+
+@Composable
+private fun HapticVibrationCard(
+ vibrateEnabled: Boolean,
+ onVibrateChange: (Boolean) -> 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.outlineVariant.copy(alpha = 0.35f)),
+ elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .size(42.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.surfaceVariant),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.PhoneAndroid,
+ contentDescription = null,
+ tint = if (vibrateEnabled) FrogoPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.size(22.dp)
+ )
+ }
+
+ Spacer(modifier = Modifier.width(14.dp))
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = "Keypress Vibration (Haptic)",
+ style = MaterialTheme.typography.titleSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 14.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Spacer(modifier = Modifier.height(2.dp))
+ Text(
+ text = "Subtle tactile pulse on key touch",
+ style = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ Switch(
+ checked = vibrateEnabled,
+ onCheckedChange = onVibrateChange,
+ colors = SwitchDefaults.colors(
+ checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
+ checkedTrackColor = FrogoPrimary
+ )
+ )
+ }
+ }
+}
+
+@Composable
+private fun LiveSoundPlaygroundCard(
+ selectedSoundType: MechanicalSoundType,
+ soundEnabled: Boolean,
+ onKeyTap: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Card(
+ modifier = modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(18.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f)
+ ),
+ border = BorderStroke(1.dp, FrogoPrimary.copy(alpha = 0.3f)),
+ elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = "LIVE MECHANICAL KEYBOARD PLAYGROUND",
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 11.sp,
+ letterSpacing = 0.8.sp
+ ),
+ color = FrogoPrimary
+ )
+
+ Surface(
+ shape = RoundedCornerShape(6.dp),
+ color = FrogoPrimary.copy(alpha = 0.15f)
+ ) {
+ Text(
+ text = "TEST ACOUSTICS",
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 9.sp
+ ),
+ color = FrogoPrimary,
+ modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(6.dp))
+
+ Text(
+ text = "Tap keycaps below to experience ${selectedSoundType.title} acoustics and tactile vibration:",
+ style = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ Spacer(modifier = Modifier.height(14.dp))
+
+ // Simulated Keycaps Row
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ listOf("Q", "W", "E", "R").forEach { keyChar ->
+ PlaygroundKeycap(
+ label = keyChar,
+ onClick = onKeyTap,
+ modifier = Modifier.weight(1f)
+ )
+ }
+
+ PlaygroundKeycap(
+ label = "SPACE",
+ onClick = onKeyTap,
+ modifier = Modifier.weight(2f)
+ )
+
+ PlaygroundKeycap(
+ label = "ENTER",
+ onClick = onKeyTap,
+ modifier = Modifier.weight(1.5f),
+ isAction = true
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun PlaygroundKeycap(
+ label: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ isAction: Boolean = false
+) {
+ Surface(
+ modifier = modifier
+ .height(44.dp)
+ .clip(RoundedCornerShape(10.dp))
+ .clickable(onClick = onClick),
+ shape = RoundedCornerShape(10.dp),
+ color = if (isAction) FrogoPrimary else MaterialTheme.colorScheme.surface,
+ border = BorderStroke(
+ 1.dp,
+ if (isAction) FrogoPrimary else MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
+ ),
+ shadowElevation = 2.dp
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = label,
+ style = MaterialTheme.typography.labelMedium.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = if (label.length > 2) 11.sp else 14.sp
+ ),
+ color = if (isAction) Color.White else MaterialTheme.colorScheme.onSurface,
+ textAlign = TextAlign.Center
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/sound/SoundViewModel.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/sound/SoundViewModel.kt
new file mode 100644
index 0000000..2092b52
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/sound/SoundViewModel.kt
@@ -0,0 +1,88 @@
+package com.frogobox.appkeyboard.ui.sound
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.frogobox.libkeyboard.common.sound.MechanicalSoundManager
+import com.frogobox.libkeyboard.common.sound.MechanicalSoundType
+import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard
+import com.frogobox.sdk.delegate.preference.PreferenceDelegates
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class SoundViewModel @Inject constructor(
+ application: Application,
+ private val pref: PreferenceDelegates
+) : AndroidViewModel(application) {
+
+ private val _soundEnabled = MutableLiveData()
+ val soundEnabled: LiveData = _soundEnabled
+
+ private val _selectedSoundType = MutableLiveData()
+ val selectedSoundType: LiveData = _selectedSoundType
+
+ private val _soundVolume = MutableLiveData()
+ val soundVolume: LiveData = _soundVolume
+
+ private val _vibrateEnabled = MutableLiveData()
+ val vibrateEnabled: LiveData = _vibrateEnabled
+
+ init {
+ loadSettings()
+ }
+
+ fun loadSettings() {
+ val soundOn = pref.getPrefBoolean(MechanicalSoundManager.PREF_KEYBOARD_SOUND_ENABLED, true)
+ val soundTypeId = pref.getPrefString(
+ MechanicalSoundManager.PREF_KEYBOARD_SOUND_TYPE,
+ MechanicalSoundType.CHERRY_MX_BLUE.id
+ )
+ val volume = pref.getPrefInt(MechanicalSoundManager.PREF_KEYBOARD_SOUND_VOLUME, 80)
+ val vibrateOn = pref.getPrefBoolean(MechanicalSoundManager.PREF_KEYBOARD_VIBRATE_ENABLED, true)
+
+ _soundEnabled.value = soundOn
+ _selectedSoundType.value = MechanicalSoundType.fromId(soundTypeId)
+ _soundVolume.value = volume
+ _vibrateEnabled.value = vibrateOn
+
+ ItemMainKeyboard.SOUND_ON_KEYPRESS = soundOn
+ ItemMainKeyboard.MECHANICAL_SOUND_TYPE = soundTypeId
+ ItemMainKeyboard.SOUND_VOLUME = volume / 100f
+ ItemMainKeyboard.VIBRATE_ON_KEYPRESS = vibrateOn
+ }
+
+ fun setSoundEnabled(enabled: Boolean) {
+ pref.savePrefBoolean(MechanicalSoundManager.PREF_KEYBOARD_SOUND_ENABLED, enabled)
+ ItemMainKeyboard.SOUND_ON_KEYPRESS = enabled
+ _soundEnabled.value = enabled
+ }
+
+ fun setSoundType(type: MechanicalSoundType) {
+ pref.savePrefString(MechanicalSoundManager.PREF_KEYBOARD_SOUND_TYPE, type.id)
+ ItemMainKeyboard.MECHANICAL_SOUND_TYPE = type.id
+ _selectedSoundType.value = type
+ playTestKeySound()
+ }
+
+ fun setSoundVolume(volume: Int) {
+ pref.savePrefInt(MechanicalSoundManager.PREF_KEYBOARD_SOUND_VOLUME, volume)
+ ItemMainKeyboard.SOUND_VOLUME = volume / 100f
+ _soundVolume.value = volume
+ }
+
+ fun setVibrateEnabled(enabled: Boolean) {
+ pref.savePrefBoolean(MechanicalSoundManager.PREF_KEYBOARD_VIBRATE_ENABLED, enabled)
+ ItemMainKeyboard.VIBRATE_ON_KEYPRESS = enabled
+ _vibrateEnabled.value = enabled
+ }
+
+ fun playTestKeySound() {
+ val isSoundOn = _soundEnabled.value ?: true
+ if (!isSoundOn) return
+ val soundType = _selectedSoundType.value ?: MechanicalSoundType.CHERRY_MX_BLUE
+ val volume = (_soundVolume.value ?: 80) / 100f
+ MechanicalSoundManager.getInstance(getApplication()).playKeySound(soundType, volume)
+ }
+}
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 fc3c713..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
@@ -1,16 +1,21 @@
package com.frogobox.appkeyboard.ui.test
+import android.content.Intent
import android.os.Bundle
+import android.provider.Settings
import android.util.Log
-import android.widget.ArrayAdapter
+import android.view.inputmethod.InputMethodManager
import androidx.activity.viewModels
-import com.frogobox.appkeyboard.R
-import com.frogobox.appkeyboard.common.base.BaseActivity
-import com.frogobox.appkeyboard.databinding.ActivityTestBinding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import com.frogobox.appkeyboard.common.base.BaseComposeActivity
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
-class TestActivity : BaseActivity() {
+class TestActivity : BaseComposeActivity() {
companion object {
private val TAG: String = TestActivity::class.java.simpleName
@@ -18,43 +23,88 @@ class TestActivity : BaseActivity() {
private val viewModel: TestViewModel by viewModels()
- override fun setupViewBinding(): ActivityTestBinding {
- return ActivityTestBinding.inflate(layoutInflater)
- }
+ private var isUsingFrogoKeyboard by mutableStateOf(false)
+ private var isFrogoKeyboardEnabled by mutableStateOf(false)
- override fun setupViewModel() {
- super.setupViewModel()
- viewModel.apply {
-
- }
- }
+ private val dummyOptions = listOf(
+ "Kuningan",
+ "Menteng",
+ "Pegangsaan",
+ "Kebon Jeruk",
+ "Kemang",
+ "Senayan",
+ "Sudirman",
+ "Thamrin"
+ )
override fun onCreateExt(savedInstanceState: Bundle?) {
super.onCreateExt(savedInstanceState)
- setupDetailActivity("Test Area")
if (savedInstanceState == null) {
- // Call View Model Here
- Log.d(TAG, "View Model : ${viewModel::class.java.simpleName}")
+ Log.d(TAG, "Initialized TestActivity with ViewModel: ${viewModel::class.java.simpleName}")
}
- // TODO : Add your code here
-
- }
-
- override fun initView() {
- super.initView()
- binding.apply {
- val dummy = listOf("Kuningan",
- "Menteng",
- "Menten213g",
- "Men123teng",
- "Mw23423",
- "Me123nteng",
- "Mente234234234ng",
- "Pegangsaan"
- )
- val adapterS = ArrayAdapter(this@TestActivity, R.layout.item_spinner, R.id.tv_text, dummy)
- etTextAuto.setAdapter(adapterS)
+ updateKeyboardStatus()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ updateKeyboardStatus()
+ }
+
+ override fun onWindowFocusChanged(hasFocus: Boolean) {
+ super.onWindowFocusChanged(hasFocus)
+ if (hasFocus) {
+ updateKeyboardStatus()
}
}
+ private fun isUsingKeyboard(): Boolean {
+ val currentKeyboard = Settings.Secure.getString(contentResolver, Settings.Secure.DEFAULT_INPUT_METHOD)
+ val targetKeyboard = "$packageName/com.frogobox.appkeyboard.services.KeyboardIME"
+ return currentKeyboard == targetKeyboard
+ }
+
+ private fun isKeyboardEnabled(): Boolean {
+ val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
+ val enabledKeyboards = inputMethodManager.enabledInputMethodList
+ return enabledKeyboards.any {
+ it.serviceInfo.packageName == packageName
+ }
+ }
+
+ private fun updateKeyboardStatus() {
+ isUsingFrogoKeyboard = isUsingKeyboard()
+ isFrogoKeyboardEnabled = isKeyboardEnabled()
+ }
+
+ @Composable
+ override fun Content() {
+ val sandboxText by viewModel.sandboxText.collectAsState()
+ val metrics by viewModel.metrics.collectAsState()
+ val activeTab by viewModel.activeTab.collectAsState()
+ val autoTextList by viewModel.autoTextList.collectAsState()
+
+ TestScreen(
+ sandboxText = sandboxText,
+ metrics = metrics,
+ activeTab = activeTab,
+ autoTextList = autoTextList,
+ isKeyboardActive = isUsingFrogoKeyboard,
+ dummyOptions = dummyOptions,
+ onTextChanged = viewModel::onTextChanged,
+ onInsertText = viewModel::onInsertText,
+ onClearText = viewModel::onClearText,
+ onResetTimer = viewModel::onResetTimer,
+ onTabSelected = viewModel::onTabSelected,
+ onChangeKeyboard = {
+ (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager).showInputMethodPicker()
+ },
+ onGoToSettings = {
+ val intent = Intent(Settings.ACTION_INPUT_METHOD_SETTINGS).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ startActivity(intent)
+ },
+ onBackClick = { finish() }
+ )
+ }
}
\ No newline at end of file
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
new file mode 100644
index 0000000..7f82c6b
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/test/TestScreen.kt
@@ -0,0 +1,765 @@
+package com.frogobox.appkeyboard.ui.test
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.widget.Toast
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.horizontalScroll
+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.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.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
+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.ContentCopy
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.Keyboard
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.Settings
+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
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ElevatedCard
+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
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.PrimaryTabRow
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Tab
+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.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.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.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.frogobox.appkeyboard.model.AutoTextEntity
+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.FrogoStatusWarning
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoTopAppBar
+
+/**
+ * Modern Material 3 Keyboard Testing Playground & Sandbox
+ * Redesigned for TASK-010 by Tim Mobile
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TestScreen(
+ sandboxText: String,
+ metrics: TypingMetrics,
+ activeTab: Int,
+ autoTextList: List,
+ isKeyboardActive: Boolean,
+ dummyOptions: List = emptyList(),
+ onTextChanged: (String) -> Unit,
+ onInsertText: (String) -> Unit,
+ onClearText: () -> Unit,
+ onResetTimer: () -> Unit,
+ onTabSelected: (Int) -> Unit,
+ onChangeKeyboard: () -> Unit,
+ onGoToSettings: () -> Unit,
+ onBackClick: (() -> Unit)? = null,
+ modifier: Modifier = Modifier
+) {
+ val context = LocalContext.current
+ val scrollState = rememberScrollState()
+
+ // Local states for specialized input tab
+ var searchInput by remember { mutableStateOf("") }
+ var numberInput by remember { mutableStateOf("") }
+ var phoneInput by remember { mutableStateOf("") }
+ var passwordInput by remember { mutableStateOf("") }
+ var passwordVisible by remember { mutableStateOf(false) }
+ var autoDropdownText by remember { mutableStateOf("") }
+ var dropdownExpanded by remember { mutableStateOf(false) }
+
+ val filteredDropdownOptions = remember(autoDropdownText, dummyOptions) {
+ if (autoDropdownText.isBlank()) {
+ dummyOptions
+ } else {
+ dummyOptions.filter { it.contains(autoDropdownText, ignoreCase = true) }
+ }
+ }
+
+ FrogoKeyboardTheme {
+ Scaffold(
+ modifier = modifier.fillMaxSize(),
+ topBar = {
+ FrogoTopAppBar(
+ title = "Test Area",
+ onBackClick = onBackClick
+ )
+ }
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .verticalScroll(scrollState)
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ verticalArrangement = Arrangement.spacedBy(14.dp)
+ ) {
+ // 1. Active Keyboard Status Card
+ KeyboardStatusCard(
+ isFrogoActive = isKeyboardActive,
+ onChangeKeyboard = onChangeKeyboard,
+ onGoToSettings = onGoToSettings
+ )
+
+ // 2. Real-time Live Typing Metrics Card
+ LiveMetricsCard(metrics = metrics)
+
+ // 3. Tab Navigation Row
+ PrimaryTabRow(
+ selectedTabIndex = activeTab,
+ containerColor = MaterialTheme.colorScheme.surface,
+ contentColor = FrogoPrimary
+ ) {
+ Tab(
+ selected = activeTab == 0,
+ onClick = { onTabSelected(0) },
+ text = { Text("Sandbox", fontWeight = FontWeight.SemiBold) },
+ icon = { Icon(Icons.Default.Edit, contentDescription = null, modifier = Modifier.size(20.dp)) }
+ )
+ Tab(
+ selected = activeTab == 1,
+ onClick = { onTabSelected(1) },
+ text = { Text("Inputs", fontWeight = FontWeight.SemiBold) },
+ icon = { Icon(Icons.Default.Keyboard, contentDescription = null, modifier = Modifier.size(20.dp)) }
+ )
+ Tab(
+ selected = activeTab == 2,
+ onClick = { onTabSelected(2) },
+ text = { Text("AutoText", fontWeight = FontWeight.SemiBold) },
+ icon = { Icon(Icons.AutoMirrored.Filled.Notes, contentDescription = null, modifier = Modifier.size(20.dp)) }
+ )
+ }
+
+ // 4. Tab Body Content
+ when (activeTab) {
+ 0 -> SandboxTabContent(
+ text = sandboxText,
+ onTextChanged = onTextChanged,
+ onInsertText = onInsertText,
+ onClearText = onClearText,
+ onResetTimer = onResetTimer,
+ onCopyText = {
+ 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()
+ }
+ }
+ )
+ 1 -> SpecializedInputsTabContent(
+ searchInput = searchInput,
+ onSearchChange = { searchInput = it },
+ numberInput = numberInput,
+ onNumberChange = { numberInput = it },
+ phoneInput = phoneInput,
+ onPhoneChange = { phoneInput = it },
+ passwordInput = passwordInput,
+ onPasswordChange = { passwordInput = it },
+ passwordVisible = passwordVisible,
+ onTogglePasswordVisibility = { passwordVisible = !passwordVisible },
+ autoDropdownText = autoDropdownText,
+ onDropdownTextChange = { autoDropdownText = it },
+ dropdownExpanded = dropdownExpanded,
+ onDropdownExpandedChange = { dropdownExpanded = it },
+ dropdownOptions = filteredDropdownOptions
+ )
+ 2 -> AutoTextTabContent(
+ snippets = autoTextList,
+ onSnippetClicked = { snippet ->
+ onInsertText(snippet.body)
+ Toast.makeText(context, "Inserted '${snippet.title}' to sandbox", Toast.LENGTH_SHORT).show()
+ onTabSelected(0)
+ }
+ )
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ }
+ }
+}
+
+/**
+ * 1. Active Keyboard Status Card Component
+ */
+@Composable
+private fun KeyboardStatusCard(
+ isFrogoActive: Boolean,
+ onChangeKeyboard: () -> Unit,
+ onGoToSettings: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val statusColor = if (isFrogoActive) FrogoStatusSuccess else FrogoStatusWarning
+ val statusTitle = if (isFrogoActive) "Frogo Keyboard Active" else "Other Keyboard Active"
+ val statusDesc = if (isFrogoActive) {
+ "Frogo Keyboard is default. Testing real keyboard layouts."
+ } else {
+ "External keyboard is active. Switch to test Frogo features."
+ }
+ val icon = if (isFrogoActive) Icons.Default.CheckCircle else Icons.Default.Warning
+
+ Card(
+ modifier = modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = statusColor.copy(alpha = 0.08f)
+ ),
+ border = BorderStroke(1.dp, statusColor.copy(alpha = 0.25f))
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(14.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .size(38.dp)
+ .clip(CircleShape)
+ .background(statusColor.copy(alpha = 0.16f)),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = statusColor,
+ modifier = Modifier.size(22.dp)
+ )
+ }
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = statusTitle,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Text(
+ text = statusDesc,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Button(
+ onClick = onChangeKeyboard,
+ modifier = Modifier.weight(1f),
+ shape = RoundedCornerShape(10.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)
+ }
+
+ OutlinedButton(
+ onClick = onGoToSettings,
+ shape = RoundedCornerShape(10.dp)
+ ) {
+ Icon(Icons.Default.Settings, contentDescription = null, modifier = Modifier.size(16.dp))
+ Spacer(modifier = Modifier.width(4.dp))
+ Text("Settings", fontSize = 13.sp)
+ }
+ }
+ }
+ }
+}
+
+/**
+ * 2. Real-time Live Typing Metrics Card Component
+ */
+@Composable
+private fun LiveMetricsCard(
+ metrics: TypingMetrics,
+ modifier: Modifier = Modifier
+) {
+ ElevatedCard(
+ modifier = modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.elevatedCardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ ),
+ elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.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 = "Live Typing Analytics",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ text = "${metrics.elapsedSeconds}s elapsed",
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ MetricItem(label = "Characters", value = "${metrics.characterCount}", modifier = Modifier.weight(1f))
+ MetricItem(label = "Words", value = "${metrics.wordCount}", modifier = Modifier.weight(1f))
+ MetricItem(label = "Lines", value = "${metrics.lineCount}", modifier = Modifier.weight(1f))
+ MetricItem(
+ label = "Typing Speed",
+ value = "${metrics.wordsPerMinute} WPM",
+ highlight = true,
+ modifier = Modifier.weight(1.2f)
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun MetricItem(
+ label: String,
+ value: String,
+ highlight: Boolean = false,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = value,
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.ExtraBold,
+ color = if (highlight) FrogoPrimary else MaterialTheme.colorScheme.onSurface
+ )
+ Text(
+ text = label,
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+}
+
+/**
+ * 3. Tab 0: Freeform Sandbox & Preset Pangrams
+ */
+@Composable
+private fun SandboxTabContent(
+ text: String,
+ onTextChanged: (String) -> Unit,
+ onInsertText: (String) -> Unit,
+ onClearText: () -> Unit,
+ onResetTimer: () -> Unit,
+ onCopyText: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val pangramsScroll = rememberScrollState()
+
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ // Preset Pangrams horizontal chips
+ Text(
+ text = "Quick Presets & Pangrams",
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ fontWeight = FontWeight.SemiBold
+ )
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .horizontalScroll(pangramsScroll),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ AssistChip(
+ onClick = { onInsertText("The quick brown fox jumps over the lazy dog") },
+ label = { Text("English Pangram") }
+ )
+ AssistChip(
+ onClick = { onInsertText("Keluarga Jefri bernomor fax tujuh belas") },
+ label = { Text("Indonesian Pangram") }
+ )
+ AssistChip(
+ onClick = { onInsertText("1234567890 !@#$%^&*()_+") },
+ label = { Text("Numbers & Symbols") }
+ )
+ AssistChip(
+ onClick = { onInsertText("😀🚀✨🎉🔥👍💯❤️") },
+ label = { Text("Emoji Palette") }
+ )
+ }
+
+ // Multiline Editor Area
+ OutlinedTextField(
+ value = text,
+ 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),
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = FrogoPrimary,
+ focusedContainerColor = MaterialTheme.colorScheme.surface,
+ unfocusedContainerColor = MaterialTheme.colorScheme.surface
+ )
+ )
+
+ // Action Toolbar: Copy, Clear, Reset Timer
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ OutlinedButton(
+ onClick = onCopyText,
+ modifier = Modifier.weight(1f),
+ shape = RoundedCornerShape(10.dp)
+ ) {
+ Icon(Icons.Default.ContentCopy, contentDescription = null, modifier = Modifier.size(16.dp))
+ Spacer(modifier = Modifier.width(6.dp))
+ Text("Copy", fontSize = 13.sp)
+ }
+
+ OutlinedButton(
+ onClick = onResetTimer,
+ modifier = Modifier.weight(1f),
+ shape = RoundedCornerShape(10.dp)
+ ) {
+ Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(16.dp))
+ Spacer(modifier = Modifier.width(6.dp))
+ Text("Reset WPM", fontSize = 13.sp)
+ }
+
+ Button(
+ onClick = onClearText,
+ modifier = Modifier.weight(1f),
+ shape = RoundedCornerShape(10.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer,
+ contentColor = MaterialTheme.colorScheme.onErrorContainer
+ )
+ ) {
+ Icon(Icons.Default.Delete, contentDescription = null, modifier = Modifier.size(16.dp))
+ Spacer(modifier = Modifier.width(6.dp))
+ Text("Clear", fontSize = 13.sp)
+ }
+ }
+ }
+}
+
+/**
+ * 4. Tab 1: Specialized Input Types (Search, Number, Phone, Password, AutoComplete)
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun SpecializedInputsTabContent(
+ searchInput: String,
+ onSearchChange: (String) -> Unit,
+ numberInput: String,
+ onNumberChange: (String) -> Unit,
+ phoneInput: String,
+ onPhoneChange: (String) -> Unit,
+ passwordInput: String,
+ onPasswordChange: (String) -> Unit,
+ passwordVisible: Boolean,
+ onTogglePasswordVisibility: () -> Unit,
+ autoDropdownText: String,
+ onDropdownTextChange: (String) -> Unit,
+ dropdownExpanded: Boolean,
+ onDropdownExpandedChange: (Boolean) -> Unit,
+ dropdownOptions: List,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(14.dp)
+ ) {
+ // Search Input
+ OutlinedTextField(
+ value = searchInput,
+ onValueChange = onSearchChange,
+ modifier = Modifier.fillMaxWidth(),
+ label = { Text("Search Input (ImeAction.Search)") },
+ placeholder = { Text("Search keywords...") },
+ leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
+ trailingIcon = {
+ if (searchInput.isNotEmpty()) {
+ IconButton(onClick = { onSearchChange("") }) {
+ Icon(Icons.Default.Clear, contentDescription = "Clear")
+ }
+ }
+ },
+ singleLine = true,
+ shape = RoundedCornerShape(14.dp),
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Text,
+ imeAction = ImeAction.Search
+ )
+ )
+
+ // Number Input
+ OutlinedTextField(
+ value = numberInput,
+ onValueChange = onNumberChange,
+ modifier = Modifier.fillMaxWidth(),
+ label = { Text("Numeric Keypad (KeyboardType.Number)") },
+ placeholder = { Text("12345678") },
+ singleLine = true,
+ shape = RoundedCornerShape(14.dp),
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Number,
+ imeAction = ImeAction.Next
+ )
+ )
+
+ // Phone Input
+ OutlinedTextField(
+ value = phoneInput,
+ onValueChange = onPhoneChange,
+ modifier = Modifier.fillMaxWidth(),
+ label = { Text("Phone Keypad (KeyboardType.Phone)") },
+ placeholder = { Text("+62 812-3456-7890") },
+ singleLine = true,
+ shape = RoundedCornerShape(14.dp),
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Phone,
+ imeAction = ImeAction.Next
+ )
+ )
+
+ // Password Input
+ OutlinedTextField(
+ value = passwordInput,
+ onValueChange = onPasswordChange,
+ modifier = Modifier.fillMaxWidth(),
+ label = { Text("Password (Visibility Toggle)") },
+ placeholder = { Text("Enter secret password...") },
+ singleLine = true,
+ shape = RoundedCornerShape(14.dp),
+ visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Password,
+ imeAction = ImeAction.Done
+ ),
+ 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")
+ }
+ }
+ )
+
+ // AutoComplete Dropdown
+ ExposedDropdownMenuBox(
+ expanded = dropdownExpanded,
+ onExpandedChange = onDropdownExpandedChange,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ OutlinedTextField(
+ value = autoDropdownText,
+ onValueChange = {
+ onDropdownTextChange(it)
+ onDropdownExpandedChange(true)
+ },
+ modifier = Modifier
+ .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true)
+ .fillMaxWidth(),
+ label = { Text("Searchable Dropdown (Filter Chips)") },
+ placeholder = { Text("Type city name...") },
+ trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = dropdownExpanded) },
+ shape = RoundedCornerShape(14.dp),
+ colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors()
+ )
+
+ if (dropdownOptions.isNotEmpty()) {
+ ExposedDropdownMenu(
+ expanded = dropdownExpanded,
+ onDismissRequest = { onDropdownExpandedChange(false) }
+ ) {
+ dropdownOptions.forEach { option ->
+ DropdownMenuItem(
+ text = { Text(option) },
+ onClick = {
+ onDropdownTextChange(option)
+ onDropdownExpandedChange(false)
+ },
+ contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * 5. Tab 2: AutoText Snippets Showcase (Tap to Insert)
+ */
+@Composable
+private fun AutoTextTabContent(
+ snippets: List,
+ onSnippetClicked: (AutoTextEntity) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Text(
+ text = "Tap any AutoText snippet to insert it into the test sandbox:",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ snippets.forEach { snippet ->
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { onSnippetClicked(snippet) },
+ shape = RoundedCornerShape(14.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ ),
+ border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(14.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .size(36.dp)
+ .clip(CircleShape)
+ .background(FrogoPrimary.copy(alpha = 0.12f)),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.Notes,
+ contentDescription = null,
+ tint = FrogoPrimary,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ Column(modifier = Modifier.weight(1f)) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ text = snippet.title,
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = FontWeight.Bold
+ )
+ Box(
+ modifier = Modifier
+ .clip(RoundedCornerShape(6.dp))
+ .background(MaterialTheme.colorScheme.surfaceVariant)
+ .padding(horizontal = 6.dp, vertical = 2.dp)
+ ) {
+ Text(
+ text = snippet.label.name,
+ fontSize = 10.sp,
+ fontWeight = FontWeight.SemiBold,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(2.dp))
+
+ Text(
+ text = snippet.body,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+ }
+ }
+ }
+}
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 cd31092..67dfd24 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
@@ -1,24 +1,124 @@
package com.frogobox.appkeyboard.ui.test
+import androidx.lifecycle.viewModelScope
import com.frogobox.appkeyboard.common.base.BaseViewModel
+import com.frogobox.appkeyboard.model.AutoTextEntity
+import com.frogobox.appkeyboard.model.AutoTextLabelType
import com.frogobox.appkeyboard.repository.autotext.AutoTextRepository
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 javax.inject.Inject
/**
* Created by Faisal Amir on 24/10/22
- * -----------------------------------------
- * E-mail : faisalamircs@gmail.com
- * Github : github.com/amirisback
- * -----------------------------------------
- * Copyright (C) Frogobox ID / amirisback
- * All rights reserved
+ * Redesigned for TASK-010 by Tim Mobile
*/
-
@HiltViewModel
class TestViewModel @Inject constructor(
private val repository: AutoTextRepository
-): BaseViewModel() {
+) : BaseViewModel() {
+
+ private val _sandboxText = MutableStateFlow("")
+ val sandboxText: StateFlow = _sandboxText.asStateFlow()
+
+ private val _activeTab = MutableStateFlow(0)
+ val activeTab: StateFlow = _activeTab.asStateFlow()
+
+ private val _metrics = MutableStateFlow(TypingMetrics())
+ val metrics: StateFlow = _metrics.asStateFlow()
+
+ private val _autoTextList = MutableStateFlow>(emptyList())
+ val autoTextList: StateFlow> = _autoTextList.asStateFlow()
+
+ private var typingStartTimestamp: Long = 0L
+
+ init {
+ loadAutoTextSnippets()
+ }
+
+ private fun loadAutoTextSnippets() {
+ viewModelScope.launch {
+ repository.getAutoText()
+ .catch { emit(getDefaultTestSnippets()) }
+ .collect { list ->
+ if (list.isEmpty()) {
+ _autoTextList.value = getDefaultTestSnippets()
+ } else {
+ _autoTextList.value = list
+ }
+ }
+ }
+ }
+
+ private fun getDefaultTestSnippets(): List {
+ return listOf(
+ AutoTextEntity(
+ id = -1,
+ title = "Greeting",
+ body = "Hello! Hope you are having a wonderful and productive day.",
+ label = AutoTextLabelType.DEFAULT,
+ isActive = true
+ ),
+ AutoTextEntity(
+ id = -2,
+ title = "Meeting Confirmation",
+ body = "Looking forward to our sync meeting tomorrow at 10:00 AM.",
+ label = AutoTextLabelType.DEFAULT,
+ isActive = true
+ ),
+ AutoTextEntity(
+ id = -3,
+ title = "Quick Sign-off",
+ body = "Kind regards,\nFrogo Keyboard Development Team",
+ label = AutoTextLabelType.DEFAULT,
+ isActive = true
+ ),
+ AutoTextEntity(
+ id = -4,
+ title = "Fast Response",
+ body = "Thanks for reaching out! I will review and reply as soon as possible.",
+ label = AutoTextLabelType.DEFAULT,
+ isActive = true
+ )
+ )
+ }
+
+ fun onTextChanged(newText: String) {
+ _sandboxText.value = newText
+ val now = System.currentTimeMillis()
+ if (typingStartTimestamp == 0L && newText.isNotEmpty()) {
+ typingStartTimestamp = now
+ }
+ val elapsedMillis = if (typingStartTimestamp > 0L) now - typingStartTimestamp else 0L
+ _metrics.value = TypingMetricsCalculator.calculate(newText, elapsedMillis)
+ }
+
+ fun onInsertText(textToInsert: String) {
+ val current = _sandboxText.value
+ val updated = if (current.isEmpty()) {
+ textToInsert
+ } else {
+ "$current\n$textToInsert"
+ }
+ onTextChanged(updated)
+ }
+
+ fun onClearText() {
+ _sandboxText.value = ""
+ typingStartTimestamp = 0L
+ _metrics.value = TypingMetrics()
+ }
+ fun onResetTimer() {
+ typingStartTimestamp = System.currentTimeMillis()
+ _metrics.value = TypingMetricsCalculator.calculate(_sandboxText.value, 0L)
+ }
+ fun onTabSelected(tabIndex: Int) {
+ _activeTab.value = tabIndex
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/test/TypingMetricsCalculator.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/test/TypingMetricsCalculator.kt
new file mode 100644
index 0000000..cdebeca
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/test/TypingMetricsCalculator.kt
@@ -0,0 +1,47 @@
+package com.frogobox.appkeyboard.ui.test
+
+import kotlin.math.roundToInt
+
+data class TypingMetrics(
+ val characterCount: Int = 0,
+ val wordCount: Int = 0,
+ val lineCount: Int = 0,
+ val wordsPerMinute: Int = 0,
+ val elapsedSeconds: Long = 0L
+)
+
+object TypingMetricsCalculator {
+
+ fun calculate(text: String, elapsedMillis: Long): TypingMetrics {
+ val charCount = text.length
+ val trimmed = text.trim()
+ val wordCount = if (trimmed.isEmpty()) {
+ 0
+ } else {
+ trimmed.split("\\s+".toRegex()).count { it.isNotEmpty() }
+ }
+
+ val lineCount = if (text.isEmpty()) {
+ 0
+ } else {
+ text.count { it == '\n' } + 1
+ }
+
+ val elapsedSeconds = elapsedMillis / 1000L
+ val wpm = if (elapsedSeconds < 2L || wordCount == 0) {
+ 0
+ } else {
+ val minutes = elapsedSeconds / 60.0
+ val rawWpm = (wordCount / minutes).roundToInt()
+ rawWpm.coerceIn(0, 300)
+ }
+
+ return TypingMetrics(
+ characterCount = charCount,
+ wordCount = wordCount,
+ lineCount = lineCount,
+ wordsPerMinute = wpm,
+ elapsedSeconds = elapsedSeconds
+ )
+ }
+}
diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/theme/ThemeActivity.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/ThemeActivity.kt
index 0218a1c..236555b 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/ui/theme/ThemeActivity.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/ThemeActivity.kt
@@ -1,150 +1,50 @@
package com.frogobox.appkeyboard.ui.theme
import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.ViewGroup
import androidx.activity.viewModels
-import com.frogobox.appkeyboard.common.base.BaseActivity
-import com.frogobox.appkeyboard.databinding.ActivityThemeBinding
-import com.frogobox.appkeyboard.databinding.ItemThemeBinding
+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
import com.frogobox.appkeyboard.model.KeyboardThemeModel
-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.showToast
-import com.frogobox.sdk.ext.visible
-import com.frogobox.sdk.util.FrogoFunc
-import com.frogobox.sdk.util.SimpleDialogUtil
import dagger.hilt.android.AndroidEntryPoint
-/**
- * Created by faisalamircs on 16/02/2024
- * -----------------------------------------
- * Name : Muhammad Faisal Amir
- * E-mail : faisalamircs@gmail.com
- * Github : github.com/amirisback
- * -----------------------------------------
- */
-
-
@AndroidEntryPoint
-class ThemeActivity : BaseActivity(),
- IFrogoBindingAdapter {
-
- companion object {
- private val TAG: String = ThemeActivity::class.java.simpleName
- }
+class ThemeActivity : BaseComposeActivity() {
private val viewModel: ThemeViewModel by viewModels()
- override fun setupViewBinding(): ActivityThemeBinding {
- return ActivityThemeBinding.inflate(layoutInflater)
- }
-
- override fun setupViewModel() {
- super.setupViewModel()
- viewModel.keyboardThemeState.observe(this) {
- setupRV(it)
- }
- }
-
+ private var themeList by mutableStateOf>(emptyList())
+ private var activeThemeVersion by mutableIntStateOf(0)
override fun onCreateExt(savedInstanceState: Bundle?) {
super.onCreateExt(savedInstanceState)
- setupDetailActivity("Setup Theme")
- if (savedInstanceState == null) {
- // Call View Model Here
- viewModel.getThemeData()
- }
- // TODO : Add your code here
-
- }
-
- override fun initView() {
- super.initView()
- binding.apply {
-
- }
- }
-
- override fun onItemClicked(
- binding: ItemThemeBinding,
- data: KeyboardThemeModel,
- position: Int,
- notifyListener: FrogoRecyclerNotifyListener
- ) {
- applyTheme(data)
- }
-
- override fun onItemLongClicked(
- binding: ItemThemeBinding,
- data: KeyboardThemeModel,
- position: Int,
- notifyListener: FrogoRecyclerNotifyListener
- ) {}
-
- override fun areContentsTheSame(
- oldItem: KeyboardThemeModel,
- newItem: KeyboardThemeModel
- ): Boolean {
- return oldItem == newItem
- }
-
- override fun areItemsTheSame(
- oldItem: KeyboardThemeModel,
- newItem: KeyboardThemeModel
- ): Boolean {
- return oldItem.name == newItem.name
- }
-
- override fun setViewBinding(parent: ViewGroup): ItemThemeBinding {
- return ItemThemeBinding.inflate(
- LayoutInflater.from(parent.context),
- parent,
- false
- )
- }
-
- override fun setupInitComponent(
- binding: ItemThemeBinding,
- data: KeyboardThemeModel,
- position: Int,
- notifyListener: FrogoRecyclerNotifyListener
- ) {
- binding.ivBackground.setImageResource(data.background)
- binding.tvTitle.text = data.name
- binding.tvDesc.text = data.description
- if (viewModel.getThemeColor() == data.background) {
- binding.ivIcon.visible()
- } else {
- binding.ivIcon.gone()
+ viewModel.keyboardThemeState.observe(this) {
+ themeList = it
+ activeThemeVersion++
}
- }
-
- private fun setupRV(data: List) {
- binding.root.injectorBinding()
- .addData(data)
- .addCallback(this)
- .createLayoutLinearVertical(false)
- .build()
- }
-
- private fun applyTheme(data: KeyboardThemeModel) {
- SimpleDialogUtil.create(
- this@ThemeActivity,
- "Change Theme",
- "Are you sure you want to change keyboard theme?",
- object : SimpleDialogUtil.OnDialogClickListener {
- override fun negativeButton() {}
- override fun positiveButton() {
- viewModel.setThemeColor(data) {
- viewModel.getThemeData()
- showToast("${data.name} Theme Applied")
- }
+ viewModel.getThemeData()
+ }
+
+ @Composable
+ override fun Content() {
+ val version = activeThemeVersion
+ ThemeScreen(
+ themeList = themeList,
+ checkIsActive = { theme ->
+ version >= 0 && viewModel.isThemeActive(theme)
+ },
+ onApplyTheme = { theme ->
+ viewModel.setThemeColor(theme) {
+ activeThemeVersion++
+ viewModel.getThemeData()
+ showToast("${theme.name} Theme Applied")
}
- }
+ },
+ onBackClick = { finish() }
)
}
-
}
\ No newline at end of file
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
new file mode 100644
index 0000000..b63daf8
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/ThemeScreen.kt
@@ -0,0 +1,1103 @@
+package com.frogobox.appkeyboard.ui.theme
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.horizontalScroll
+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.aspectRatio
+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.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+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
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+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
+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
+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.layout.ContentScale
+import androidx.compose.ui.res.colorResource
+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.R
+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.FrogoTopAppBar
+
+enum class ThemeCategory(val title: String) {
+ ALL("All"),
+ SOLID("Solid Colors"),
+ WALLPAPER("Wallpapers")
+}
+
+@Composable
+fun ThemeScreen(
+ themeList: List,
+ checkIsActive: (KeyboardThemeModel) -> Boolean,
+ onApplyTheme: (KeyboardThemeModel) -> Unit,
+ onBackClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ var selectedCategory by remember { mutableStateOf(ThemeCategory.ALL) }
+ var previewedTheme by remember { mutableStateOf(null) }
+ var confirmApplyDialogTheme by remember { mutableStateOf(null) }
+
+ // Synchronize preview with active theme on initial load or change
+ LaunchedEffect(themeList) {
+ if (previewedTheme == null && themeList.isNotEmpty()) {
+ previewedTheme = themeList.firstOrNull { checkIsActive(it) } ?: themeList.first()
+ }
+ }
+
+ // Filter themes based on selected category chip
+ val filteredThemes = remember(themeList, selectedCategory) {
+ when (selectedCategory) {
+ ThemeCategory.ALL -> themeList
+ ThemeCategory.SOLID -> themeList.filter { it.themType == ThemeType.COLOR }
+ ThemeCategory.WALLPAPER -> themeList.filter { it.themType == ThemeType.IMAGE }
+ }
+ }
+
+ val activeTheme = themeList.firstOrNull { checkIsActive(it) }
+
+ Scaffold(
+ modifier = modifier.fillMaxSize(),
+ topBar = {
+ FrogoTopAppBar(
+ title = "Keyboard Themes",
+ onBackClick = onBackClick
+ )
+ },
+ containerColor = MaterialTheme.colorScheme.background
+ ) { paddingValues ->
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(2),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentPadding = PaddingValues(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ // 1. Live Interactive Keyboard Preview Hero Section (Spans all columns)
+ item(span = { GridItemSpan(2) }) {
+ val currentPreview = previewedTheme ?: activeTheme ?: themeList.firstOrNull()
+ currentPreview?.let { theme ->
+ val isPreviewActive = checkIsActive(theme)
+ ThemePreviewHero(
+ theme = theme,
+ isActive = isPreviewActive,
+ onApplyClick = {
+ confirmApplyDialogTheme = theme
+ }
+ )
+ }
+ }
+
+ // 2. Category Filter Chips Section (Spans all columns)
+ item(span = { GridItemSpan(2) }) {
+ ThemeCategoryFilterRow(
+ selectedCategory = selectedCategory,
+ onCategorySelected = { selectedCategory = it },
+ totalAllCount = themeList.size,
+ solidCount = themeList.count { it.themType == ThemeType.COLOR },
+ wallpaperCount = themeList.count { it.themType == ThemeType.IMAGE }
+ )
+ }
+
+ // 3. Section Title (Spans all columns)
+ item(span = { GridItemSpan(2) }) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 4.dp, bottom = 2.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = "Theme Gallery (${filteredThemes.size})",
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 16.sp
+ ),
+ color = MaterialTheme.colorScheme.onBackground
+ )
+
+ activeTheme?.let {
+ Text(
+ text = "Active: ${it.name}",
+ style = MaterialTheme.typography.labelMedium.copy(
+ fontWeight = FontWeight.SemiBold,
+ color = FrogoPrimary
+ )
+ )
+ }
+ }
+ }
+
+ // 4. 2-Column Responsive Theme Cards
+ items(filteredThemes, key = { it.name }) { theme ->
+ val isActive = checkIsActive(theme)
+ val isPreviewed = previewedTheme?.name == theme.name
+
+ ThemeGridCard(
+ theme = theme,
+ isActive = isActive,
+ isPreviewed = isPreviewed,
+ onCardClick = {
+ previewedTheme = theme
+ },
+ onApplyClick = {
+ confirmApplyDialogTheme = theme
+ }
+ )
+ }
+
+ // Bottom Spacer for edge-to-edge breathing room
+ item(span = { GridItemSpan(2) }) {
+ Spacer(modifier = Modifier.height(24.dp))
+ }
+ }
+ }
+
+ // Confirmation Dialog before applying theme
+ confirmApplyDialogTheme?.let { theme ->
+ AlertDialog(
+ onDismissRequest = { confirmApplyDialogTheme = null },
+ icon = {
+ Surface(
+ shape = CircleShape,
+ color = FrogoPrimary.copy(alpha = 0.12f),
+ modifier = Modifier.size(48.dp)
+ ) {
+ Box(contentAlignment = Alignment.Center) {
+ Icon(
+ imageVector = Icons.Default.Palette,
+ contentDescription = null,
+ tint = FrogoPrimary,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+ }
+ },
+ title = {
+ Text(
+ text = "Apply Theme",
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.Bold
+ ),
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+ },
+ text = {
+ Text(
+ text = "Set \"${theme.name}\" as your active keyboard theme? All keyboard keys and panels will adopt this style.",
+ style = MaterialTheme.typography.bodyMedium,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+ },
+ confirmButton = {
+ Button(
+ onClick = {
+ val current = theme
+ confirmApplyDialogTheme = null
+ onApplyTheme(current)
+ previewedTheme = current
+ },
+ colors = ButtonDefaults.buttonColors(containerColor = FrogoPrimary),
+ shape = RoundedCornerShape(10.dp)
+ ) {
+ Text(
+ text = "Apply Now",
+ fontWeight = FontWeight.Bold
+ )
+ }
+ },
+ dismissButton = {
+ TextButton(
+ onClick = { confirmApplyDialogTheme = null }
+ ) {
+ Text("Cancel")
+ }
+ },
+ shape = RoundedCornerShape(20.dp)
+ )
+ }
+}
+
+/**
+ * Backwards-compatible overload matching previous signature
+ */
+@Composable
+fun ThemeScreen(
+ themeList: List,
+ activeThemeColor: Int,
+ onApplyTheme: (KeyboardThemeModel) -> Unit,
+ onBackClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ ThemeScreen(
+ themeList = themeList,
+ checkIsActive = { it.background == activeThemeColor },
+ onApplyTheme = onApplyTheme,
+ onBackClick = onBackClick,
+ modifier = modifier
+ )
+}
+
+/**
+ * 1. Live Interactive Keyboard Preview Hero Component
+ */
+@Composable
+private fun ThemePreviewHero(
+ theme: KeyboardThemeModel,
+ isActive: Boolean,
+ onApplyClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Card(
+ modifier = modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(20.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 3.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)
+ )
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ // Header Row: Theme Label + Status / Apply Action
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Surface(
+ shape = CircleShape,
+ color = FrogoPrimary.copy(alpha = 0.12f),
+ modifier = Modifier.size(32.dp)
+ ) {
+ Box(contentAlignment = Alignment.Center) {
+ Icon(
+ imageVector = Icons.Default.Palette,
+ contentDescription = null,
+ tint = FrogoPrimary,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.width(10.dp))
+
+ Column {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = theme.name,
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 16.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Spacer(modifier = Modifier.width(6.dp))
+ Surface(
+ shape = RoundedCornerShape(6.dp),
+ color = MaterialTheme.colorScheme.surfaceVariant
+ ) {
+ Text(
+ text = if (theme.themType == ThemeType.COLOR) "Color" else "Wallpaper",
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontSize = 10.sp,
+ fontWeight = FontWeight.SemiBold
+ ),
+ modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ Text(
+ text = theme.description,
+ style = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+
+ if (isActive) {
+ Surface(
+ shape = RoundedCornerShape(12.dp),
+ color = Color(0xFF41AF60).copy(alpha = 0.12f),
+ border = BorderStroke(1.dp, Color(0xFF41AF60).copy(alpha = 0.4f))
+ ) {
+ Row(
+ modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Default.CheckCircle,
+ contentDescription = "Active",
+ tint = Color(0xFF41AF60),
+ modifier = Modifier.size(16.dp)
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ Text(
+ text = "Active",
+ style = MaterialTheme.typography.labelMedium.copy(
+ fontWeight = FontWeight.Bold,
+ color = Color(0xFF41AF60),
+ fontSize = 12.sp
+ )
+ )
+ }
+ }
+ } else {
+ Button(
+ onClick = onApplyClick,
+ colors = ButtonDefaults.buttonColors(containerColor = FrogoPrimary),
+ shape = RoundedCornerShape(12.dp),
+ contentPadding = PaddingValues(horizontal = 14.dp, vertical = 6.dp),
+ modifier = Modifier.height(34.dp)
+ ) {
+ Text(
+ text = "Apply",
+ style = MaterialTheme.typography.labelMedium.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 12.sp
+ )
+ )
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.height(14.dp))
+
+ // Realistic Keyboard Mockup Canvas
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(14.dp))
+ .border(
+ width = 1.dp,
+ color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f),
+ shape = RoundedCornerShape(14.dp)
+ )
+ ) {
+ // Background Layer (Color or Wallpaper)
+ when (theme.themType) {
+ ThemeType.COLOR -> {
+ Box(
+ modifier = Modifier
+ .matchParentSize()
+ .background(colorResource(id = theme.background))
+ )
+ }
+ ThemeType.IMAGE -> {
+ Image(
+ painter = painterResource(id = theme.background),
+ contentDescription = theme.name,
+ modifier = Modifier.matchParentSize(),
+ contentScale = ContentScale.Crop
+ )
+ }
+ }
+
+ // Keyboard Layout Overlay (Utility Strip + Keycaps)
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp, vertical = 10.dp),
+ verticalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ // Top Suggestion & Toolbar Strip
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 2.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ MockToolIcon(Icons.Default.SentimentSatisfied)
+ MockToolIcon(Icons.Default.TextFields)
+ MockToolIcon(Icons.Default.Language)
+ MockToolIcon(Icons.Default.Settings)
+ }
+
+ Text(
+ text = "Frogo Keyboard",
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 10.sp,
+ color = Color.White.copy(alpha = 0.85f)
+ )
+ )
+ }
+
+ // Keycap Row 1: Q W E R T Y U I O P (10 keys)
+ MockKeyRow(keys = listOf("Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"))
+
+ // Keycap Row 2: A S D F G H J K L (9 keys with inset margins)
+ MockKeyRow(
+ keys = listOf("A", "S", "D", "F", "G", "H", "J", "K", "L"),
+ horizontalPadding = 12.dp
+ )
+
+ // Keycap Row 3: Shift, Z X C V B N M, Backspace
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ MockTextKey(
+ text = "⇧",
+ weight = 1.3f
+ )
+ listOf("Z", "X", "C", "V", "B", "N", "M").forEach { letter ->
+ MockKey(
+ text = letter,
+ modifier = Modifier.weight(1f)
+ )
+ }
+ MockSpecialKey(
+ icon = Icons.AutoMirrored.Filled.Backspace,
+ weight = 1.3f
+ )
+ }
+
+ // Keycap Row 4: ?123, Language, Spacebar, Enter
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ MockTextKey(text = "?123", weight = 1.4f)
+ MockIconKey(icon = Icons.Default.Language, weight = 1f)
+ MockSpacebarKey(text = "English", weight = 4.2f)
+ MockActionKey(icon = Icons.AutoMirrored.Filled.KeyboardReturn, weight = 1.6f)
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * 2. Category Filter Chips Row Component
+ */
+@Composable
+private fun ThemeCategoryFilterRow(
+ selectedCategory: ThemeCategory,
+ onCategorySelected: (ThemeCategory) -> Unit,
+ totalAllCount: Int,
+ solidCount: Int,
+ wallpaperCount: Int,
+ modifier: Modifier = Modifier
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .horizontalScroll(rememberScrollState()),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ ThemeCategory.entries.forEach { category ->
+ val count = when (category) {
+ ThemeCategory.ALL -> totalAllCount
+ ThemeCategory.SOLID -> solidCount
+ ThemeCategory.WALLPAPER -> wallpaperCount
+ }
+ val isSelected = selectedCategory == category
+
+ FilterChip(
+ selected = isSelected,
+ onClick = { onCategorySelected(category) },
+ label = {
+ Text(
+ text = "${category.title} ($count)",
+ style = MaterialTheme.typography.labelMedium.copy(
+ fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium
+ )
+ )
+ },
+ leadingIcon = {
+ when (category) {
+ ThemeCategory.ALL -> Icon(
+ imageVector = Icons.Default.Palette,
+ contentDescription = null,
+ modifier = Modifier.size(16.dp)
+ )
+ ThemeCategory.SOLID -> Icon(
+ imageVector = Icons.Default.CheckCircle,
+ contentDescription = null,
+ modifier = Modifier.size(16.dp)
+ )
+ ThemeCategory.WALLPAPER -> Icon(
+ imageVector = Icons.Default.SentimentSatisfied,
+ contentDescription = null,
+ modifier = Modifier.size(16.dp)
+ )
+ }
+ },
+ colors = FilterChipDefaults.filterChipColors(
+ selectedContainerColor = FrogoPrimary.copy(alpha = 0.15f),
+ selectedLabelColor = FrogoPrimary,
+ selectedLeadingIconColor = FrogoPrimary
+ ),
+ shape = RoundedCornerShape(12.dp)
+ )
+ }
+ }
+}
+
+/**
+ * 3. 2-Column Visual Theme Card Component
+ */
+@Composable
+private fun ThemeGridCard(
+ theme: KeyboardThemeModel,
+ isActive: Boolean,
+ isPreviewed: Boolean,
+ onCardClick: () -> Unit,
+ onApplyClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val borderColor by animateColorAsState(
+ targetValue = when {
+ isActive -> FrogoPrimary
+ isPreviewed -> FrogoPrimary.copy(alpha = 0.6f)
+ else -> MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
+ },
+ animationSpec = tween(durationMillis = 200),
+ label = "cardBorderColor"
+ )
+
+ Card(
+ modifier = modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(16.dp))
+ .clickable { onCardClick() },
+ shape = RoundedCornerShape(16.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = if (isPreviewed || isActive) 3.dp else 1.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ ),
+ border = BorderStroke(
+ width = if (isActive || isPreviewed) 2.dp else 1.dp,
+ color = borderColor
+ )
+ ) {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ // Top Preview Swatch / Wallpaper Area
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(92.dp)
+ ) {
+ when (theme.themType) {
+ ThemeType.COLOR -> {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(colorResource(id = theme.background))
+ )
+ }
+ ThemeType.IMAGE -> {
+ Image(
+ painter = painterResource(id = theme.background),
+ contentDescription = theme.name,
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
+ )
+ }
+ }
+
+ // Mini Keycap Silhouette Overlay for Authentic Keyboard Feel
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 8.dp, vertical = 6.dp),
+ verticalArrangement = Arrangement.spacedBy(3.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ repeat(10) {
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .height(14.dp)
+ .clip(RoundedCornerShape(2.dp))
+ .background(Color.White.copy(alpha = 0.28f))
+ )
+ }
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 4.dp),
+ horizontalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ repeat(9) {
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .height(14.dp)
+ .clip(RoundedCornerShape(2.dp))
+ .background(Color.White.copy(alpha = 0.28f))
+ )
+ }
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .weight(1.3f)
+ .height(14.dp)
+ .clip(RoundedCornerShape(2.dp))
+ .background(Color.White.copy(alpha = 0.35f))
+ )
+ repeat(7) {
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .height(14.dp)
+ .clip(RoundedCornerShape(2.dp))
+ .background(Color.White.copy(alpha = 0.28f))
+ )
+ }
+ Box(
+ modifier = Modifier
+ .weight(1.3f)
+ .height(14.dp)
+ .clip(RoundedCornerShape(2.dp))
+ .background(Color.White.copy(alpha = 0.35f))
+ )
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .weight(1.5f)
+ .height(14.dp)
+ .clip(RoundedCornerShape(2.dp))
+ .background(Color.White.copy(alpha = 0.28f))
+ )
+ Box(
+ modifier = Modifier
+ .weight(4.5f)
+ .height(14.dp)
+ .clip(RoundedCornerShape(2.dp))
+ .background(Color.White.copy(alpha = 0.38f))
+ )
+ Box(
+ modifier = Modifier
+ .weight(2f)
+ .height(14.dp)
+ .clip(RoundedCornerShape(2.dp))
+ .background(FrogoPrimary.copy(alpha = 0.75f))
+ )
+ }
+ }
+
+ // Floating Active Badge (Top Right)
+ if (isActive) {
+ Surface(
+ shape = RoundedCornerShape(bottomStart = 10.dp),
+ color = Color(0xFF41AF60),
+ modifier = Modifier.align(Alignment.TopEnd)
+ ) {
+ Row(
+ modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Default.Check,
+ contentDescription = "Active",
+ tint = Color.White,
+ modifier = Modifier.size(12.dp)
+ )
+ Spacer(modifier = Modifier.width(3.dp))
+ Text(
+ text = "Active",
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 10.sp,
+ color = Color.White
+ )
+ )
+ }
+ }
+ } else if (isPreviewed) {
+ Surface(
+ shape = RoundedCornerShape(bottomStart = 10.dp),
+ color = FrogoPrimary,
+ modifier = Modifier.align(Alignment.TopEnd)
+ ) {
+ Text(
+ text = "Previewing",
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 9.sp,
+ color = Color.White
+ ),
+ modifier = Modifier.padding(horizontal = 6.dp, vertical = 3.dp)
+ )
+ }
+ }
+ }
+
+ // Bottom Info Container
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 12.dp, vertical = 10.dp)
+ ) {
+ Text(
+ text = theme.name,
+ style = MaterialTheme.typography.titleSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 14.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurface,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ Spacer(modifier = Modifier.height(2.dp))
+
+ Text(
+ text = theme.description,
+ style = MaterialTheme.typography.bodySmall.copy(
+ fontSize = 11.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+ }
+}
+
+// --------------------------------------------------------------------------------
+// KEYBOARD MOCKUP HELPER COMPOSABLES
+// --------------------------------------------------------------------------------
+
+@Composable
+private fun MockToolIcon(
+ icon: ImageVector,
+ modifier: Modifier = Modifier
+) {
+ Surface(
+ shape = CircleShape,
+ color = Color.Black.copy(alpha = 0.25f),
+ modifier = modifier.size(20.dp)
+ ) {
+ Box(contentAlignment = Alignment.Center) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = Color.White.copy(alpha = 0.9f),
+ modifier = Modifier.size(12.dp)
+ )
+ }
+ }
+}
+
+@Composable
+private fun MockKeyRow(
+ keys: List,
+ horizontalPadding: androidx.compose.ui.unit.Dp = 0.dp,
+ modifier: Modifier = Modifier
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = horizontalPadding),
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ keys.forEach { letter ->
+ MockKey(
+ text = letter,
+ modifier = Modifier.weight(1f)
+ )
+ }
+ }
+}
+
+@Composable
+private fun MockKey(
+ text: String,
+ modifier: Modifier = Modifier
+) {
+ Surface(
+ shape = RoundedCornerShape(5.dp),
+ color = Color.White.copy(alpha = 0.22f),
+ border = BorderStroke(0.5.dp, Color.White.copy(alpha = 0.35f)),
+ modifier = modifier.height(26.dp)
+ ) {
+ Box(contentAlignment = Alignment.Center) {
+ Text(
+ text = text,
+ style = MaterialTheme.typography.labelMedium.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 11.sp,
+ color = Color.White
+ )
+ )
+ }
+ }
+}
+
+@Composable
+private fun MockSpecialKey(
+ icon: ImageVector,
+ weight: Float,
+ modifier: Modifier = Modifier
+) {
+ Surface(
+ shape = RoundedCornerShape(5.dp),
+ color = Color.Black.copy(alpha = 0.25f),
+ border = BorderStroke(0.5.dp, Color.White.copy(alpha = 0.3f)),
+ modifier = modifier
+ .height(26.dp)
+ .then(Modifier.run { this })
+ ) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.padding(horizontal = 6.dp)
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = Color.White,
+ modifier = Modifier.size(14.dp)
+ )
+ }
+ }
+}
+
+@Composable
+private fun MockTextKey(
+ text: String,
+ weight: Float,
+ modifier: Modifier = Modifier
+) {
+ Surface(
+ shape = RoundedCornerShape(5.dp),
+ color = Color.Black.copy(alpha = 0.25f),
+ border = BorderStroke(0.5.dp, Color.White.copy(alpha = 0.3f)),
+ modifier = modifier.height(26.dp)
+ ) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.padding(horizontal = 6.dp)
+ ) {
+ Text(
+ text = text,
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 10.sp,
+ color = Color.White
+ )
+ )
+ }
+ }
+}
+
+@Composable
+private fun MockIconKey(
+ icon: ImageVector,
+ weight: Float,
+ modifier: Modifier = Modifier
+) {
+ Surface(
+ shape = RoundedCornerShape(5.dp),
+ color = Color.Black.copy(alpha = 0.25f),
+ border = BorderStroke(0.5.dp, Color.White.copy(alpha = 0.3f)),
+ modifier = modifier.height(26.dp)
+ ) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.padding(horizontal = 6.dp)
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = Color.White,
+ modifier = Modifier.size(13.dp)
+ )
+ }
+ }
+}
+
+@Composable
+private fun MockSpacebarKey(
+ text: String,
+ weight: Float,
+ modifier: Modifier = Modifier
+) {
+ Surface(
+ shape = RoundedCornerShape(5.dp),
+ color = Color.White.copy(alpha = 0.28f),
+ border = BorderStroke(0.5.dp, Color.White.copy(alpha = 0.4f)),
+ modifier = modifier.height(26.dp)
+ ) {
+ Box(contentAlignment = Alignment.Center) {
+ Text(
+ text = text,
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.Medium,
+ fontSize = 10.sp,
+ color = Color.White.copy(alpha = 0.9f)
+ )
+ )
+ }
+ }
+}
+
+@Composable
+private fun MockActionKey(
+ icon: ImageVector,
+ weight: Float,
+ modifier: Modifier = Modifier
+) {
+ Surface(
+ shape = RoundedCornerShape(5.dp),
+ color = FrogoPrimary,
+ modifier = modifier.height(26.dp)
+ ) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.padding(horizontal = 8.dp)
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = Color.White,
+ modifier = Modifier.size(14.dp)
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true, name = "ThemeScreen Light Preview")
+@Composable
+fun ThemeScreenPreview() {
+ FrogoKeyboardTheme(darkTheme = false) {
+ ThemeScreen(
+ themeList = listOf(
+ KeyboardThemeModel("Default", "Classic Adaptive", ThemeType.COLOR, R.color.color_bg_keyboard_default),
+ KeyboardThemeModel("Frogo Purple", "Signature Brand", ThemeType.COLOR, R.color.color_bg_keyboard_purple),
+ KeyboardThemeModel("Midnight AMOLED", "Deep OLED Black", ThemeType.COLOR, R.color.color_bg_keyboard_dark),
+ KeyboardThemeModel("Ocean Blue", "Calm & Focused", ThemeType.COLOR, R.color.color_bg_keyboard_blue),
+ KeyboardThemeModel("Forest Emerald", "Natural Harmony", ThemeType.COLOR, R.color.color_bg_keyboard_green),
+ KeyboardThemeModel("Crimson Sunset", "Vibrant Warmth", ThemeType.COLOR, R.color.color_bg_keyboard_red),
+ KeyboardThemeModel("Sunset Orange", "Energetic Twilight", ThemeType.COLOR, R.color.color_bg_keyboard_orange),
+ KeyboardThemeModel("Nordic Cyan", "Fresh & Clean", ThemeType.COLOR, R.color.color_bg_keyboard_cyan),
+ KeyboardThemeModel("Sakura Pink", "Aesthetic Pastel", ThemeType.COLOR, R.color.color_bg_keyboard_pink),
+ KeyboardThemeModel("Amber Gold", "Golden Accent", ThemeType.COLOR, R.color.color_bg_keyboard_yellow),
+ KeyboardThemeModel("Wallpaper", "Sample Artwork", ThemeType.IMAGE, R.drawable.ic_wallpaper_dummy)
+ ),
+ checkIsActive = { it.name == "Frogo Purple" },
+ onApplyTheme = {},
+ onBackClick = {}
+ )
+ }
+}
+
+@Preview(showBackground = true, name = "ThemeScreen Dark Preview")
+@Composable
+fun ThemeScreenDarkPreview() {
+ FrogoKeyboardTheme(darkTheme = true) {
+ ThemeScreen(
+ themeList = listOf(
+ KeyboardThemeModel("Default", "Classic Adaptive", ThemeType.COLOR, R.color.color_bg_keyboard_default),
+ KeyboardThemeModel("Frogo Purple", "Signature Brand", ThemeType.COLOR, R.color.color_bg_keyboard_purple),
+ KeyboardThemeModel("Midnight AMOLED", "Deep OLED Black", ThemeType.COLOR, R.color.color_bg_keyboard_dark),
+ KeyboardThemeModel("Wallpaper", "Sample Artwork", ThemeType.IMAGE, R.drawable.ic_wallpaper_dummy)
+ ),
+ checkIsActive = { it.name == "Midnight AMOLED" },
+ onApplyTheme = {},
+ onBackClick = {}
+ )
+ }
+}
diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/theme/ThemeViewModel.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/ThemeViewModel.kt
index 2dcd26e..d80199b 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/ui/theme/ThemeViewModel.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/ThemeViewModel.kt
@@ -5,8 +5,8 @@ import androidx.lifecycle.MutableLiveData
import com.frogobox.appkeyboard.R
import com.frogobox.appkeyboard.common.base.BaseViewModel
import com.frogobox.appkeyboard.model.KeyboardThemeModel
+import com.frogobox.appkeyboard.model.ThemeType
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
@@ -20,7 +20,6 @@ import javax.inject.Inject
* -----------------------------------------
*/
-
@HiltViewModel
class ThemeViewModel @Inject constructor(
private val pref: PreferenceDelegates,
@@ -38,6 +37,14 @@ class ThemeViewModel @Inject constructor(
return pref.getPrefInt(KeyboardUtil.KEYBOARD_COLOR, R.color.color_bg_keyboard_default)
}
+ fun getThemeType(): String {
+ return pref.getPrefString(KeyboardUtil.KEYBOARD_COLOR_TYPE, ThemeType.COLOR.name)
+ }
+
+ fun isThemeActive(data: KeyboardThemeModel): Boolean {
+ return data.background == getThemeColor() && data.themType.name == getThemeType()
+ }
+
fun setThemeColor(data: KeyboardThemeModel, onSuccess: () -> Unit) {
pref.savePrefString(KeyboardUtil.KEYBOARD_COLOR_TYPE, data.themType.name)
pref.savePrefInt(KeyboardUtil.KEYBOARD_COLOR, data.background)
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
new file mode 100644
index 0000000..c50fb02
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/Color.kt
@@ -0,0 +1,27 @@
+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)
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
new file mode 100644
index 0000000..1d6d5ce
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/FrogoEmptyView.kt
@@ -0,0 +1,67 @@
+package com.frogobox.appkeyboard.ui.theme.compose
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+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.material.icons.Icons
+import androidx.compose.material.icons.filled.Info
+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.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.
+ */
+@Composable
+fun FrogoEmptyView(
+ title: String = "NO DATA FOUND",
+ subtitle: String = "Please insert data",
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(32.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.Info,
+ contentDescription = null,
+ modifier = Modifier.size(72.dp),
+ tint = MaterialTheme.colorScheme.outline
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ text = title.uppercase(),
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 18.sp
+ ),
+ color = MaterialTheme.colorScheme.onBackground,
+ textAlign = TextAlign.Center
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ text = subtitle,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ textAlign = TextAlign.Center
+ )
+ }
+}
diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/FrogoTopAppBar.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/FrogoTopAppBar.kt
new file mode 100644
index 0000000..520c08c
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/FrogoTopAppBar.kt
@@ -0,0 +1,56 @@
+package com.frogobox.appkeyboard.ui.theme.compose
+
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+
+/**
+ * Reusable Material 3 TopAppBar for Frogo Keyboard app screens.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun FrogoTopAppBar(
+ title: String,
+ modifier: Modifier = Modifier,
+ onBackClick: (() -> Unit)? = null,
+ actions: @Composable RowScope.() -> Unit = {}
+) {
+ TopAppBar(
+ title = {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleLarge.copy(
+ fontWeight = FontWeight.Bold
+ ),
+ color = MaterialTheme.colorScheme.onBackground
+ )
+ },
+ modifier = modifier,
+ navigationIcon = {
+ if (onBackClick != null) {
+ IconButton(onClick = onBackClick) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back",
+ tint = MaterialTheme.colorScheme.onBackground
+ )
+ }
+ }
+ },
+ actions = actions,
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.background,
+ scrolledContainerColor = MaterialTheme.colorScheme.surface
+ )
+ )
+}
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
new file mode 100644
index 0000000..805fb5b
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/Theme.kt
@@ -0,0 +1,48 @@
+package com.frogobox.appkeyboard.ui.theme.compose
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+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),
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = FrogoPrimary,
+ secondary = FrogoSecondary,
+ tertiary = FrogoPrimaryVariant,
+ background = FrogoBackground,
+ surface = Color.White,
+ onPrimary = Color.White,
+ onSecondary = Color.Black,
+ onTertiary = Color.White,
+ onBackground = FrogoText,
+ onSurface = FrogoText,
+)
+
+@Composable
+fun FrogoKeyboardTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable () -> Unit
+) {
+ val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/Type.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/Type.kt
new file mode 100644
index 0000000..1be1f70
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/theme/compose/Type.kt
@@ -0,0 +1,45 @@
+package com.frogobox.appkeyboard.ui.theme.compose
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+val Typography = Typography(
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Bold,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ titleMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 18.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.15.sp
+ ),
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ ),
+ bodyMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.25.sp
+ ),
+ labelLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.1.sp
+ )
+)
diff --git a/app/src/main/java/com/frogobox/appkeyboard/ui/toggle/ToggleActivity.kt b/app/src/main/java/com/frogobox/appkeyboard/ui/toggle/ToggleActivity.kt
index d5d55ff..5e665b2 100644
--- a/app/src/main/java/com/frogobox/appkeyboard/ui/toggle/ToggleActivity.kt
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/toggle/ToggleActivity.kt
@@ -1,107 +1,45 @@
package com.frogobox.appkeyboard.ui.toggle
import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.ViewGroup
import androidx.activity.viewModels
-import com.frogobox.appkeyboard.databinding.ActivityToggleBinding
-import com.frogobox.appkeyboard.databinding.ItemToggleBinding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateMapOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import com.frogobox.appkeyboard.common.base.BaseComposeActivity
import com.frogobox.appkeyboard.model.KeyboardFeatureModel
-import com.frogobox.appkeyboard.ui.main.BaseMainActivity
-import com.frogobox.recycler.core.FrogoRecyclerNotifyListener
-import com.frogobox.recycler.core.IFrogoBindingAdapter
-import com.frogobox.recycler.ext.injectorBinding
import dagger.hilt.android.AndroidEntryPoint
-/**
- * Created by Faisal Amir on 24/10/22
- * -----------------------------------------
- * E-mail : faisalamircs@gmail.com
- * Github : github.com/amirisback
- * -----------------------------------------
- * Copyright (C) Frogobox ID / amirisback
- * All rights reserved
- */
-
-
@AndroidEntryPoint
-class ToggleActivity : BaseMainActivity() {
+class ToggleActivity : BaseComposeActivity() {
private val viewModel: ToggleViewModel by viewModels()
- override fun setupViewBinding(): ActivityToggleBinding {
- return ActivityToggleBinding.inflate(layoutInflater)
- }
+ private var featureList by mutableStateOf>(emptyList())
+ private val toggleStates = mutableStateMapOf()
override fun onCreateExt(savedInstanceState: Bundle?) {
super.onCreateExt(savedInstanceState)
- if (savedInstanceState == null) {
- viewModel.getKeyboardFeatureData()
+ viewModel.keyboardFeatureState.observe(this) { list ->
+ featureList = list
+ list.forEach { feature ->
+ toggleStates[feature.id] = viewModel.getSwitchToggle(feature.id)
+ }
}
+ viewModel.getKeyboardFeatureData()
}
- override fun initView() {
- super.initView()
- setupDetailActivity("Toggle Feature")
- }
-
- override fun setupViewModel() {
- super.setupViewModel()
- viewModel.keyboardFeatureState.observe(this) {
- binding.root.injectorBinding()
- .addData(it)
- .addCallback(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): ItemToggleBinding {
- return ItemToggleBinding.inflate(
- LayoutInflater.from(parent.context),
- parent,
- false
- )
- }
-
- override fun setupInitComponent(
- binding: ItemToggleBinding,
- data: KeyboardFeatureModel,
- position: Int,
- notifyListener: FrogoRecyclerNotifyListener,
- ) {
- binding.swToggle.setOnCheckedChangeListener(null)
- binding.ivIcon.setImageResource(data.icon)
- binding.tvTitle.text = data.text
- binding.swToggle.isChecked = viewModel.getSwitchToggle(data.id)
- binding.swToggle.setOnCheckedChangeListener { _, isChecked ->
- viewModel.switchToggle(data.id, isChecked)
- }
- }
-
- override fun onItemClicked(
- binding: ItemToggleBinding,
- data: KeyboardFeatureModel,
- position: Int,
- notifyListener: FrogoRecyclerNotifyListener,
- ) {}
-
-
-
- })
- .createLayoutGrid(2)
- .build()
- }
+ @Composable
+ override fun Content() {
+ ToggleScreen(
+ features = featureList,
+ getToggleState = { id -> toggleStates[id] ?: viewModel.getSwitchToggle(id) },
+ onToggleChanged = { id, isChecked ->
+ toggleStates[id] = isChecked
+ viewModel.switchToggle(id, isChecked)
+ },
+ onBackClick = { finish() }
+ )
}
-
}
\ No newline at end of file
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
new file mode 100644
index 0000000..f9de6e9
--- /dev/null
+++ b/app/src/main/java/com/frogobox/appkeyboard/ui/toggle/ToggleScreen.kt
@@ -0,0 +1,550 @@
+package com.frogobox.appkeyboard.ui.toggle
+
+import androidx.compose.foundation.BorderStroke
+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.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.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Switch
+import androidx.compose.material3.SwitchDefaults
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+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
+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.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.frogobox.appkeyboard.R
+import com.frogobox.appkeyboard.model.KeyboardFeatureModel
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoKeyboardTheme
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoStatusSuccess
+import com.frogobox.appkeyboard.ui.theme.compose.FrogoTopAppBar
+
+enum class ToggleFilterTab {
+ ALL,
+ ACTIVE,
+ DISABLED
+}
+
+@Composable
+fun ToggleScreen(
+ features: List,
+ getToggleState: (String) -> Boolean,
+ onToggleChanged: (String, Boolean) -> Unit,
+ onBackClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ var selectedTab by remember { mutableStateOf(ToggleFilterTab.ALL) }
+
+ val activeCount = features.count { getToggleState(it.id) }
+ val disabledCount = features.size - activeCount
+
+ val filteredFeatures = when (selectedTab) {
+ ToggleFilterTab.ALL -> features
+ ToggleFilterTab.ACTIVE -> features.filter { getToggleState(it.id) }
+ ToggleFilterTab.DISABLED -> features.filter { !getToggleState(it.id) }
+ }
+
+ Scaffold(
+ modifier = modifier.fillMaxSize(),
+ topBar = {
+ FrogoTopAppBar(
+ title = "Toggle Feature",
+ onBackClick = onBackClick
+ )
+ },
+ containerColor = MaterialTheme.colorScheme.background
+ ) { paddingValues ->
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(2),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 12.dp, bottom = 28.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ // Header summary banner
+ item(span = { GridItemSpan(maxLineSpan) }) {
+ ToggleHeaderBanner(
+ totalCount = features.size,
+ activeCount = activeCount,
+ onEnableAll = {
+ features.forEach { onToggleChanged(it.id, true) }
+ },
+ onDisableAll = {
+ features.forEach { onToggleChanged(it.id, false) }
+ }
+ )
+ }
+
+ // Quick Filter Chips
+ item(span = { GridItemSpan(maxLineSpan) }) {
+ ToggleFilterRow(
+ selectedTab = selectedTab,
+ totalCount = features.size,
+ activeCount = activeCount,
+ disabledCount = disabledCount,
+ onTabSelected = { selectedTab = it }
+ )
+ }
+
+ if (filteredFeatures.isEmpty()) {
+ item(span = { GridItemSpan(maxLineSpan) }) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 40.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(
+ text = if (selectedTab == ToggleFilterTab.ACTIVE) {
+ "No active features found"
+ } else {
+ "No disabled features found"
+ },
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ TextButton(onClick = { selectedTab = ToggleFilterTab.ALL }) {
+ Text(
+ text = "Show All Features",
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.SemiBold
+ )
+ }
+ }
+ }
+ }
+ } else {
+ items(filteredFeatures, key = { it.id }) { feature ->
+ ToggleFeatureCard(
+ feature = feature,
+ isChecked = getToggleState(feature.id),
+ onCheckedChange = { isChecked ->
+ onToggleChanged(feature.id, isChecked)
+ }
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun ToggleHeaderBanner(
+ totalCount: Int,
+ activeCount: Int,
+ onEnableAll: () -> Unit,
+ onDisableAll: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Card(
+ modifier = modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(18.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ ),
+ border = BorderStroke(
+ width = 1.dp,
+ color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f)
+ ),
+ elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = "Keyboard Toolbar",
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 16.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Spacer(modifier = Modifier.height(2.dp))
+ Text(
+ text = "Manage shortcuts displayed on your keyboard",
+ style = MaterialTheme.typography.bodySmall.copy(
+ fontSize = 12.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ Surface(
+ shape = RoundedCornerShape(20.dp),
+ color = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
+ ) {
+ Text(
+ text = "$activeCount/$totalCount Active",
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 11.sp
+ ),
+ color = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.End,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ TextButton(
+ onClick = onEnableAll,
+ contentPadding = PaddingValues(horizontal = 8.dp, vertical = 2.dp)
+ ) {
+ Text(
+ text = "Enable All",
+ style = MaterialTheme.typography.labelMedium.copy(
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 12.sp
+ ),
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
+
+ Spacer(modifier = Modifier.width(4.dp))
+
+ TextButton(
+ onClick = onDisableAll,
+ contentPadding = PaddingValues(horizontal = 8.dp, vertical = 2.dp)
+ ) {
+ Text(
+ text = "Disable All",
+ style = MaterialTheme.typography.labelMedium.copy(
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp
+ ),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun ToggleFilterRow(
+ selectedTab: ToggleFilterTab,
+ totalCount: Int,
+ activeCount: Int,
+ disabledCount: Int,
+ onTabSelected: (ToggleFilterTab) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(vertical = 2.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ ToggleFilterChip(
+ text = "All ($totalCount)",
+ isSelected = selectedTab == ToggleFilterTab.ALL,
+ onClick = { onTabSelected(ToggleFilterTab.ALL) }
+ )
+
+ ToggleFilterChip(
+ text = "Active ($activeCount)",
+ isSelected = selectedTab == ToggleFilterTab.ACTIVE,
+ onClick = { onTabSelected(ToggleFilterTab.ACTIVE) }
+ )
+
+ ToggleFilterChip(
+ text = "Disabled ($disabledCount)",
+ isSelected = selectedTab == ToggleFilterTab.DISABLED,
+ onClick = { onTabSelected(ToggleFilterTab.DISABLED) }
+ )
+ }
+}
+
+@Composable
+private fun ToggleFilterChip(
+ text: String,
+ isSelected: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Surface(
+ modifier = modifier
+ .clip(RoundedCornerShape(20.dp))
+ .clickable(onClick = onClick),
+ shape = RoundedCornerShape(20.dp),
+ color = if (isSelected) {
+ MaterialTheme.colorScheme.primary
+ } else {
+ MaterialTheme.colorScheme.surface
+ },
+ border = BorderStroke(
+ width = 1.dp,
+ color = if (isSelected) {
+ MaterialTheme.colorScheme.primary
+ } else {
+ MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f)
+ }
+ )
+ ) {
+ Text(
+ text = text,
+ style = MaterialTheme.typography.labelMedium.copy(
+ fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium,
+ fontSize = 12.sp
+ ),
+ color = if (isSelected) {
+ MaterialTheme.colorScheme.onPrimary
+ } else {
+ MaterialTheme.colorScheme.onSurfaceVariant
+ },
+ modifier = Modifier.padding(horizontal = 14.dp, vertical = 6.dp)
+ )
+ }
+}
+
+@Composable
+private fun ToggleFeatureCard(
+ feature: KeyboardFeatureModel,
+ isChecked: Boolean,
+ onCheckedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val primaryColor = MaterialTheme.colorScheme.primary
+ val surfaceColor = MaterialTheme.colorScheme.surface
+ val outlineVariant = MaterialTheme.colorScheme.outlineVariant
+
+ Card(
+ modifier = modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(18.dp))
+ .clickable { onCheckedChange(!isChecked) },
+ shape = RoundedCornerShape(18.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
+ border = BorderStroke(
+ width = 1.dp,
+ color = if (isChecked) {
+ primaryColor.copy(alpha = 0.45f)
+ } else {
+ outlineVariant.copy(alpha = 0.35f)
+ }
+ ),
+ colors = CardDefaults.cardColors(
+ containerColor = if (isChecked) {
+ primaryColor.copy(alpha = 0.08f)
+ } else {
+ surfaceColor
+ }
+ )
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(14.dp)
+ ) {
+ // Top row with Icon Container and Switch
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .size(42.dp)
+ .clip(RoundedCornerShape(12.dp))
+ .background(
+ if (isChecked) {
+ primaryColor.copy(alpha = 0.15f)
+ } else {
+ MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
+ }
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Image(
+ painter = painterResource(id = feature.icon),
+ contentDescription = feature.text,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+
+ Switch(
+ checked = isChecked,
+ onCheckedChange = onCheckedChange,
+ colors = SwitchDefaults.colors(
+ checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
+ checkedTrackColor = primaryColor,
+ uncheckedThumbColor = MaterialTheme.colorScheme.outline,
+ uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
+ uncheckedBorderColor = Color.Transparent
+ )
+ )
+ }
+
+ Spacer(modifier = Modifier.height(14.dp))
+
+ Text(
+ text = feature.text,
+ style = MaterialTheme.typography.titleSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontSize = 14.sp
+ ),
+ color = if (isChecked) {
+ MaterialTheme.colorScheme.onSurface
+ } else {
+ MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
+ },
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .size(6.dp)
+ .clip(CircleShape)
+ .background(
+ if (isChecked) {
+ FrogoStatusSuccess
+ } else {
+ MaterialTheme.colorScheme.outline.copy(alpha = 0.4f)
+ }
+ )
+ )
+
+ Spacer(modifier = Modifier.width(6.dp))
+
+ Text(
+ text = if (isChecked) "Active" else "Disabled",
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontWeight = if (isChecked) FontWeight.SemiBold else FontWeight.Normal,
+ fontSize = 11.sp
+ ),
+ color = if (isChecked) {
+ FrogoStatusSuccess
+ } else {
+ MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
+ }
+ )
+ }
+ }
+ }
+}
+
+@Preview(name = "Light Mode", showBackground = true)
+@Composable
+fun ToggleScreenLightPreview() {
+ FrogoKeyboardTheme(darkTheme = false) {
+ ToggleScreen(
+ features = listOf(
+ KeyboardFeatureModel(
+ id = "auto_text",
+ text = "Auto Text",
+ icon = R.drawable.ic_menu_auto_text
+ ),
+ KeyboardFeatureModel(
+ id = "app_review",
+ text = "App Review",
+ icon = R.drawable.ic_menu_ps_app
+ ),
+ KeyboardFeatureModel(
+ id = "game_review",
+ text = "Game Review",
+ icon = R.drawable.ic_menu_ps_game
+ ),
+ KeyboardFeatureModel(
+ id = "love_emoji",
+ text = "Love Emoji",
+ icon = R.drawable.ic_menu_ps_love
+ )
+ ),
+ getToggleState = { it == "auto_text" || it == "game_review" },
+ onToggleChanged = { _, _ -> },
+ onBackClick = {}
+ )
+ }
+}
+
+@Preview(name = "Dark Mode", showBackground = true)
+@Composable
+fun ToggleScreenDarkPreview() {
+ FrogoKeyboardTheme(darkTheme = true) {
+ ToggleScreen(
+ features = listOf(
+ KeyboardFeatureModel(
+ id = "auto_text",
+ text = "Auto Text",
+ icon = R.drawable.ic_menu_auto_text
+ ),
+ KeyboardFeatureModel(
+ id = "app_review",
+ text = "App Review",
+ icon = R.drawable.ic_menu_ps_app
+ ),
+ KeyboardFeatureModel(
+ id = "game_review",
+ text = "Game Review",
+ icon = R.drawable.ic_menu_ps_game
+ ),
+ KeyboardFeatureModel(
+ id = "love_emoji",
+ text = "Love Emoji",
+ icon = R.drawable.ic_menu_ps_love
+ )
+ ),
+ getToggleState = { it == "auto_text" || it == "game_review" },
+ onToggleChanged = { _, _ -> },
+ onBackClick = {}
+ )
+ }
+}
diff --git a/app/src/main/res/drawable/bg_suggestion_bar.xml b/app/src/main/res/drawable/bg_suggestion_bar.xml
new file mode 100644
index 0000000..5736f68
--- /dev/null
+++ b/app/src/main/res/drawable/bg_suggestion_bar.xml
@@ -0,0 +1,13 @@
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_suggestion_pill.xml b/app/src/main/res/drawable/bg_suggestion_pill.xml
new file mode 100644
index 0000000..6af3416
--- /dev/null
+++ b/app/src/main/res/drawable/bg_suggestion_pill.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_close_suggestion.xml b/app/src/main/res/drawable/ic_close_suggestion.xml
new file mode 100644
index 0000000..fc0a6e1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_close_suggestion.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_menu_sound.xml b/app/src/main/res/drawable/ic_menu_sound.xml
new file mode 100644
index 0000000..c4fde8e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_menu_sound.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_menu_suggestion.xml b/app/src/main/res/drawable/ic_menu_suggestion.xml
new file mode 100644
index 0000000..e6c052a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_menu_suggestion.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_suggestion_switch.xml b/app/src/main/res/drawable/ic_suggestion_switch.xml
new file mode 100644
index 0000000..0629f82
--- /dev/null
+++ b/app/src/main/res/drawable/ic_suggestion_switch.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_test.xml b/app/src/main/res/layout/activity_test.xml
deleted file mode 100644
index 24deb49..0000000
--- a/app/src/main/res/layout/activity_test.xml
+++ /dev/null
@@ -1,87 +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
index 3da3016..025b3f5 100644
--- a/app/src/main/res/layout/item_keyboard_autotext.xml
+++ b/app/src/main/res/layout/item_keyboard_autotext.xml
@@ -1,24 +1,38 @@
+ android:elevation="@dimen/frogo_dimen_2dp"
+ android:orientation="vertical"
+ android:paddingStart="14dp"
+ android:paddingTop="10dp"
+ android:paddingEnd="14dp"
+ android:paddingBottom="10dp">
+ android:textStyle="bold"
+ android:textSize="14sp"
+ tools:text="Greetings" />
+
+
\ 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 2570902..164282d 100644
--- a/app/src/main/res/layout/item_keyboard_header.xml
+++ b/app/src/main/res/layout/item_keyboard_header.xml
@@ -12,8 +12,7 @@
android:id="@+id/iv_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:src="@drawable/ic_menu_news"
- app:tint="@color/tv_text" />
+ android:src="@drawable/ic_menu_news" />
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/keyboard_ime.xml b/app/src/main/res/layout/keyboard_ime.xml
index c897c68..553303b 100644
--- a/app/src/main/res/layout/keyboard_ime.xml
+++ b/app/src/main/res/layout/keyboard_ime.xml
@@ -23,6 +23,12 @@
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-night/color.xml b/app/src/main/res/values-night/color.xml
index 5a6dec7..d24e8ee 100644
--- a/app/src/main/res/values-night/color.xml
+++ b/app/src/main/res/values-night/color.xml
@@ -2,5 +2,22 @@
@color/keyboard_background
+ #7C4DFF
+ #000000
+ #1E88E5
+ #388E3C
+ #E53935
+ #F57C00
+ #00ACC1
+ #D81B60
+ #FDD835
+
+
+ #1E232A
+ #B0BEC5
+ #4FC3F7
+ #0D47A1
+ #ECEFF1
+ #37474F
\ 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 000324b..4c9034d 100644
--- a/app/src/main/res/values/color.xml
+++ b/app/src/main/res/values/color.xml
@@ -2,9 +2,22 @@
@color/keyboard_background
- #F44336
- #4CAF50
- #3F51B5
- #FFEB3B
+ #6200EE
+ #121212
+ #1976D2
+ #2E7D32
+ #C62828
+ #EF6C00
+ #00838F
+ #C2185B
+ #FBC02D
+
+
+ #F4F6F8
+ #546E7A
+ #0288D1
+ #E1F5FE
+ #37474F
+ #E0E0E0
\ No newline at end of file
diff --git a/app/src/test/java/com/frogobox/appkeyboard/AutoTextTest.kt b/app/src/test/java/com/frogobox/appkeyboard/AutoTextTest.kt
new file mode 100644
index 0000000..2574d4c
--- /dev/null
+++ b/app/src/test/java/com/frogobox/appkeyboard/AutoTextTest.kt
@@ -0,0 +1,95 @@
+package com.frogobox.appkeyboard
+
+import com.frogobox.appkeyboard.model.AutoTextEntity
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class AutoTextTest {
+
+ private val sampleList = listOf(
+ AutoTextEntity(id = 1, title = "Greeting Morning", body = "Good morning! How can I help you today?"),
+ AutoTextEntity(id = 2, title = "Bank Account", body = "BCA: 1234567890 a/n Faisal Amir"),
+ AutoTextEntity(id = 3, title = "Email Address", body = "faisal.amir@example.com"),
+ AutoTextEntity(id = 4, title = "Closing Note", body = "Thank you for reaching out. Have a wonderful day!")
+ )
+
+ @Test
+ fun testAutoTextFilteringByTitle() {
+ val query = "bank"
+ val filtered = sampleList.filter {
+ it.title.contains(query, ignoreCase = true) || it.body.contains(query, ignoreCase = true)
+ }
+ assertEquals(1, filtered.size)
+ assertEquals("Bank Account", filtered[0].title)
+ }
+
+ @Test
+ fun testAutoTextFilteringByBody() {
+ val query = "1234567890"
+ val filtered = sampleList.filter {
+ it.title.contains(query, ignoreCase = true) || it.body.contains(query, ignoreCase = true)
+ }
+ assertEquals(1, filtered.size)
+ assertEquals("Bank Account", filtered[0].title)
+ }
+
+ @Test
+ fun testAutoTextFilteringCaseInsensitive() {
+ val query = "GREETING"
+ val filtered = sampleList.filter {
+ it.title.contains(query, ignoreCase = true) || it.body.contains(query, ignoreCase = true)
+ }
+ assertEquals(1, filtered.size)
+ assertEquals(1, filtered[0].id)
+ }
+
+ @Test
+ fun testAutoTextFilteringEmptyQueryReturnsAll() {
+ val query = ""
+ val filtered = sampleList.filter {
+ it.title.contains(query, ignoreCase = true) || it.body.contains(query, ignoreCase = true)
+ }
+ assertEquals(sampleList.size, filtered.size)
+ }
+
+ @Test
+ fun testAutoTextFilteringNoMatch() {
+ val query = "xyznonexistent"
+ val filtered = sampleList.filter {
+ it.title.contains(query, ignoreCase = true) || it.body.contains(query, ignoreCase = true)
+ }
+ assertTrue(filtered.isEmpty())
+ }
+
+ @Test
+ fun testAutoTextWordCountCalculation() {
+ val text = "Good morning! How can I help you today?"
+ val wordCount = text.trim().split(Regex("\\s+")).count { it.isNotEmpty() }
+ assertEquals(8, wordCount)
+
+ val emptyText = " "
+ val emptyWordCount = if (emptyText.isBlank()) 0 else emptyText.trim().split(Regex("\\s+")).count { it.isNotEmpty() }
+ assertEquals(0, emptyWordCount)
+ }
+
+ @Test
+ fun testAutoTextCharacterCount() {
+ val body = "Hello World"
+ assertEquals(11, body.length)
+ }
+
+ @Test
+ fun testAutoTextFormValidation() {
+ val validTitle = "My Title"
+ val validBody = "My Body"
+ assertTrue(validTitle.isNotBlank() && validBody.isNotBlank())
+
+ val emptyTitle = ""
+ assertFalse(emptyTitle.isNotBlank() && validBody.isNotBlank())
+
+ val blankBody = " "
+ assertFalse(validTitle.isNotBlank() && blankBody.isNotBlank())
+ }
+}
diff --git a/app/src/test/java/com/frogobox/appkeyboard/KeyboardLanguageTest.kt b/app/src/test/java/com/frogobox/appkeyboard/KeyboardLanguageTest.kt
new file mode 100644
index 0000000..db6bc21
--- /dev/null
+++ b/app/src/test/java/com/frogobox/appkeyboard/KeyboardLanguageTest.kt
@@ -0,0 +1,181 @@
+package com.frogobox.appkeyboard
+
+import com.frogobox.appkeyboard.ui.language.KeyboardLanguage
+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 tests for KeyboardLanguage data model, metadata integrity, active resolution, and search filtering.
+ */
+class KeyboardLanguageTest {
+
+ private val sampleLanguages = listOf(
+ KeyboardLanguage(
+ name = "English (QWERTY)",
+ xml = 101,
+ layoutType = "QWERTY",
+ code = "EN",
+ script = "Latin"
+ ),
+ KeyboardLanguage(
+ name = "English (QWERTZ)",
+ xml = 102,
+ layoutType = "QWERTZ",
+ code = "EN",
+ script = "Latin"
+ ),
+ KeyboardLanguage(
+ name = "English (DVORAK)",
+ xml = 103,
+ layoutType = "DVORAK",
+ code = "EN",
+ script = "Latin"
+ ),
+ KeyboardLanguage(
+ name = "French",
+ xml = 104,
+ layoutType = "AZERTY",
+ code = "FR",
+ script = "Français (Latin)"
+ ),
+ KeyboardLanguage(
+ name = "German",
+ xml = 105,
+ layoutType = "QWERTZ",
+ code = "DE",
+ script = "Deutsch (Latin)"
+ ),
+ KeyboardLanguage(
+ name = "Russian",
+ xml = 106,
+ layoutType = "ЙЦУКЕН",
+ code = "RU",
+ script = "Русский (Cyrillic)"
+ ),
+ KeyboardLanguage(
+ name = "Persian",
+ xml = 107,
+ layoutType = "Standard",
+ code = "FA",
+ script = "فارسی (Perso-Arabic)",
+ isRtl = true
+ )
+ )
+
+ @Test
+ fun testKeyboardLanguageBackwardCompatibilityDefaults() {
+ // Constructor call without new optional parameters
+ val legacyLanguage = KeyboardLanguage(
+ name = "Legacy Language",
+ xml = 999
+ )
+
+ assertEquals("Legacy Language", legacyLanguage.name)
+ assertEquals(999, legacyLanguage.xml)
+ assertEquals("Standard", legacyLanguage.layoutType)
+ assertEquals("EN", legacyLanguage.code)
+ assertEquals("Latin", legacyLanguage.script)
+ assertFalse(legacyLanguage.isRtl)
+ }
+
+ @Test
+ fun testActiveLanguageResolution() {
+ val activeXml = 105 // German
+ val active = sampleLanguages.firstOrNull { it.xml == activeXml }
+
+ assertNotNull(active)
+ assertEquals("German", active?.name)
+ assertEquals("DE", active?.code)
+ assertEquals("QWERTZ", active?.layoutType)
+
+ val nonExistentActive = sampleLanguages.firstOrNull { it.xml == 9999 }
+ assertNull(nonExistentActive)
+ }
+
+ @Test
+ fun testSearchFilteringByName() {
+ val query = "french"
+ val results = sampleLanguages.filter {
+ it.name.lowercase().contains(query.lowercase()) ||
+ it.code.lowercase().contains(query.lowercase()) ||
+ it.layoutType.lowercase().contains(query.lowercase()) ||
+ it.script.lowercase().contains(query.lowercase())
+ }
+
+ assertEquals(1, results.size)
+ assertEquals("French", results[0].name)
+ assertEquals("FR", results[0].code)
+ }
+
+ @Test
+ fun testSearchFilteringByLayoutType() {
+ val query = "QWERTZ"
+ val results = sampleLanguages.filter {
+ it.name.lowercase().contains(query.lowercase()) ||
+ it.code.lowercase().contains(query.lowercase()) ||
+ it.layoutType.lowercase().contains(query.lowercase()) ||
+ it.script.lowercase().contains(query.lowercase())
+ }
+
+ // Both English (QWERTZ) and German (QWERTZ)
+ assertEquals(2, results.size)
+ assertTrue(results.any { it.name == "English (QWERTZ)" })
+ assertTrue(results.any { it.name == "German" })
+ }
+
+ @Test
+ fun testSearchFilteringByCode() {
+ val query = "RU"
+ val results = sampleLanguages.filter {
+ it.name.lowercase().contains(query.lowercase()) ||
+ it.code.lowercase().contains(query.lowercase()) ||
+ it.layoutType.lowercase().contains(query.lowercase()) ||
+ it.script.lowercase().contains(query.lowercase())
+ }
+
+ assertEquals(1, results.size)
+ assertEquals("Russian", results[0].name)
+ assertEquals("ЙЦУКЕН", results[0].layoutType)
+ }
+
+ @Test
+ fun testSearchFilteringByScript() {
+ val query = "cyrillic"
+ val results = sampleLanguages.filter {
+ it.name.lowercase().contains(query.lowercase()) ||
+ it.code.lowercase().contains(query.lowercase()) ||
+ it.layoutType.lowercase().contains(query.lowercase()) ||
+ it.script.lowercase().contains(query.lowercase())
+ }
+
+ assertEquals(1, results.size)
+ assertEquals("Russian", results[0].name)
+ }
+
+ @Test
+ fun testSearchFilteringNoResults() {
+ val query = "NonExistentLanguage123"
+ val results = sampleLanguages.filter {
+ it.name.lowercase().contains(query.lowercase()) ||
+ it.code.lowercase().contains(query.lowercase()) ||
+ it.layoutType.lowercase().contains(query.lowercase()) ||
+ it.script.lowercase().contains(query.lowercase())
+ }
+
+ assertTrue(results.isEmpty())
+ }
+
+ @Test
+ fun testRtlMetadataIntegrity() {
+ val persian = sampleLanguages.first { it.code == "FA" }
+ assertTrue(persian.isRtl)
+ assertEquals("Standard", persian.layoutType)
+
+ val english = sampleLanguages.first { it.code == "EN" && it.layoutType == "QWERTY" }
+ assertFalse(english.isRtl)
+ }
+}
diff --git a/app/src/test/java/com/frogobox/appkeyboard/MechanicalSoundAndProgrammerKeyboardTest.kt b/app/src/test/java/com/frogobox/appkeyboard/MechanicalSoundAndProgrammerKeyboardTest.kt
new file mode 100644
index 0000000..03a606c
--- /dev/null
+++ b/app/src/test/java/com/frogobox/appkeyboard/MechanicalSoundAndProgrammerKeyboardTest.kt
@@ -0,0 +1,126 @@
+package com.frogobox.appkeyboard
+
+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
+
+/**
+ * Unit tests for Mechanical Sound Switch Profiles (Mechvibes)
+ * and Programmer / Coding Keyboard (Codeboard style).
+ */
+class MechanicalSoundAndProgrammerKeyboardTest {
+
+ @Test
+ fun testMechanicalSoundTypeResolution() {
+ 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"))
+
+ // Case insensitivity
+ assertEquals(MechanicalSoundType.CHERRY_MX_BLUE, MechanicalSoundType.fromId("CHERRY_MX_BLUE"))
+ assertEquals(MechanicalSoundType.TYPEWRITER, MechanicalSoundType.fromId("Typewriter"))
+
+ // Fallback for null or unknown id
+ assertEquals(MechanicalSoundType.CHERRY_MX_BLUE, MechanicalSoundType.fromId(null))
+ assertEquals(MechanicalSoundType.CHERRY_MX_BLUE, MechanicalSoundType.fromId("unknown_switch"))
+ }
+
+ @Test
+ fun testMechanicalSoundTypeResources() {
+ val mechanicalProfiles = listOf(
+ MechanicalSoundType.CHERRY_MX_BLUE,
+ MechanicalSoundType.CHERRY_MX_BROWN,
+ MechanicalSoundType.CHERRY_MX_RED,
+ MechanicalSoundType.TYPEWRITER
+ )
+
+ for (profile in mechanicalProfiles) {
+ assertTrue("Raw resource ID should be valid for $profile", profile.rawResId > 0)
+ assertTrue("Title should not be empty for $profile", profile.title.isNotBlank())
+ assertTrue("Description should not be empty for $profile", profile.description.isNotBlank())
+ }
+
+ assertEquals(0, MechanicalSoundType.OFF.rawResId)
+ assertEquals(-1, MechanicalSoundType.SYSTEM_CLICK.rawResId)
+ }
+
+ @Test
+ fun testProgrammerKeycodes() {
+ assertEquals(-7, ItemMainKeyboard.KEYCODE_TAB)
+ assertEquals(-8, ItemMainKeyboard.KEYCODE_ARROW_LEFT)
+ assertEquals(-9, ItemMainKeyboard.KEYCODE_ARROW_RIGHT)
+ assertEquals(-10, ItemMainKeyboard.KEYCODE_ARROW_UP)
+ assertEquals(-11, ItemMainKeyboard.KEYCODE_ARROW_DOWN)
+
+ // Ensure keycodes are unique and negative
+ val keycodes = listOf(
+ ItemMainKeyboard.KEYCODE_SHIFT,
+ ItemMainKeyboard.KEYCODE_MODE_CHANGE,
+ ItemMainKeyboard.KEYCODE_ENTER,
+ ItemMainKeyboard.KEYCODE_DELETE,
+ ItemMainKeyboard.KEYCODE_EMOJI,
+ ItemMainKeyboard.KEYCODE_TAB,
+ ItemMainKeyboard.KEYCODE_ARROW_LEFT,
+ ItemMainKeyboard.KEYCODE_ARROW_RIGHT,
+ ItemMainKeyboard.KEYCODE_ARROW_UP,
+ ItemMainKeyboard.KEYCODE_ARROW_DOWN
+ )
+
+ assertEquals(keycodes.size, keycodes.toSet().size)
+ assertTrue(keycodes.all { it < 0 })
+ }
+
+ @Test
+ fun testProgrammerKeyboardMetadataAndSearch() {
+ val programmerKeyboard = KeyboardLanguage(
+ name = "Programmer / Coding (QWERTY)",
+ xml = com.frogobox.libkeyboard.R.xml.keys_letters_programmer,
+ layoutType = "Codeboard",
+ code = "DEV",
+ script = "Code & Symbols"
+ )
+
+ assertEquals("Programmer / Coding (QWERTY)", programmerKeyboard.name)
+ assertEquals("Codeboard", programmerKeyboard.layoutType)
+ assertEquals("DEV", programmerKeyboard.code)
+ assertEquals("Code & Symbols", programmerKeyboard.script)
+
+ val languages = listOf(
+ KeyboardLanguage("English (QWERTY)", 1, "QWERTY", "EN", "Latin"),
+ programmerKeyboard
+ )
+
+ // Search by 'programmer'
+ val searchProgrammer = languages.filter { it.name.lowercase().contains("programmer") }
+ assertEquals(1, searchProgrammer.size)
+ assertEquals("DEV", searchProgrammer[0].code)
+
+ // Search by 'codeboard'
+ val searchCodeboard = languages.filter { it.layoutType.lowercase().contains("codeboard") }
+ assertEquals(1, searchCodeboard.size)
+
+ // Search by 'dev'
+ val searchDev = languages.filter { it.code.lowercase().contains("dev") }
+ assertEquals(1, searchDev.size)
+ }
+
+ @Test
+ fun testVolumeClamping() {
+ fun clampVolume(rawVolumePercent: Int): Float {
+ return (rawVolumePercent / 100f).coerceIn(0.05f, 1.0f)
+ }
+
+ assertEquals(0.8f, clampVolume(80), 0.001f)
+ assertEquals(0.05f, clampVolume(0), 0.001f)
+ assertEquals(0.05f, clampVolume(-20), 0.001f)
+ assertEquals(1.0f, clampVolume(100), 0.001f)
+ assertEquals(1.0f, clampVolume(150), 0.001f)
+ }
+}
diff --git a/app/src/test/java/com/frogobox/appkeyboard/ThemeTest.kt b/app/src/test/java/com/frogobox/appkeyboard/ThemeTest.kt
new file mode 100644
index 0000000..dcfbc7f
--- /dev/null
+++ b/app/src/test/java/com/frogobox/appkeyboard/ThemeTest.kt
@@ -0,0 +1,96 @@
+package com.frogobox.appkeyboard
+
+import com.frogobox.appkeyboard.model.KeyboardThemeModel
+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
+
+/**
+ * Unit test for Keyboard Theme models and theme logic verification.
+ */
+class ThemeTest {
+
+ @Test
+ fun testKeyboardThemeTypeMapping() {
+ val themes = KeyboardThemeType.entries.map { it.mapToModel() }
+ assertEquals(11, themes.size)
+
+ val defaultTheme = themes.first { it.name == "Default" }
+ assertEquals(ThemeType.COLOR, defaultTheme.themType)
+ assertTrue(defaultTheme.background != 0)
+
+ val purpleTheme = themes.first { it.name == "Frogo Purple" }
+ assertEquals(ThemeType.COLOR, purpleTheme.themType)
+ assertTrue(purpleTheme.background != 0)
+
+ val imageTheme = themes.first { it.name == "Wallpaper" }
+ assertEquals(ThemeType.IMAGE, imageTheme.themType)
+ assertTrue(imageTheme.background != 0)
+ }
+
+ @Test
+ fun testKeyboardThemeTypeFrom() {
+ assertEquals(KeyboardThemeType.DEFAULT, KeyboardThemeType from "DEFAULT")
+ assertEquals(KeyboardThemeType.PURPLE, KeyboardThemeType from "PURPLE")
+ assertEquals(KeyboardThemeType.DARK, KeyboardThemeType from "DARK")
+ assertEquals(KeyboardThemeType.BLUE, KeyboardThemeType from "BLUE")
+ assertEquals(KeyboardThemeType.GREEN, KeyboardThemeType from "GREEN")
+ assertEquals(KeyboardThemeType.RED, KeyboardThemeType from "RED")
+ assertEquals(KeyboardThemeType.ORANGE, KeyboardThemeType from "ORANGE")
+ assertEquals(KeyboardThemeType.CYAN, KeyboardThemeType from "CYAN")
+ assertEquals(KeyboardThemeType.PINK, KeyboardThemeType from "PINK")
+ assertEquals(KeyboardThemeType.YELLOW, KeyboardThemeType from "YELLOW")
+ assertEquals(KeyboardThemeType.IMAGE_BG_DARK, KeyboardThemeType from "IMAGE_BG_DARK")
+ // Unknown fallback to DEFAULT
+ assertEquals(KeyboardThemeType.DEFAULT, KeyboardThemeType from "UNKNOWN_THEME")
+ }
+
+ @Test
+ fun testThemeActiveMatchingLogic() {
+ val colorTheme = KeyboardThemeModel(
+ name = "Default",
+ description = "Default Color",
+ themType = ThemeType.COLOR,
+ background = 1001
+ )
+ val imageTheme = KeyboardThemeModel(
+ name = "Image",
+ description = "Sample Wallpaper",
+ themType = ThemeType.IMAGE,
+ background = 1001 // simulate identical integer resource ID
+ )
+
+ // Matching function considering both background and type
+ fun isThemeActive(
+ theme: KeyboardThemeModel,
+ activeColor: Int,
+ activeType: String
+ ): Boolean {
+ return theme.background == activeColor && theme.themType.name == activeType
+ }
+
+ // When COLOR is active
+ assertTrue(isThemeActive(colorTheme, 1001, ThemeType.COLOR.name))
+ assertFalse(isThemeActive(imageTheme, 1001, ThemeType.COLOR.name))
+
+ // When IMAGE is active
+ assertFalse(isThemeActive(colorTheme, 1001, ThemeType.IMAGE.name))
+ assertTrue(isThemeActive(imageTheme, 1001, ThemeType.IMAGE.name))
+ }
+
+ @Test
+ fun testSafeThemeTypeParsing() {
+ val safeParse = { typeStr: String ->
+ runCatching { ThemeType.valueOf(typeStr) }.getOrDefault(ThemeType.COLOR)
+ }
+
+ assertEquals(ThemeType.COLOR, safeParse("COLOR"))
+ assertEquals(ThemeType.IMAGE, safeParse("IMAGE"))
+ assertEquals(ThemeType.COLOR, safeParse("CORRUPTED_VALUE"))
+ assertEquals(ThemeType.COLOR, safeParse(""))
+ }
+}
diff --git a/app/src/test/java/com/frogobox/appkeyboard/TypingMetricsTest.kt b/app/src/test/java/com/frogobox/appkeyboard/TypingMetricsTest.kt
new file mode 100644
index 0000000..6ffff4a
--- /dev/null
+++ b/app/src/test/java/com/frogobox/appkeyboard/TypingMetricsTest.kt
@@ -0,0 +1,80 @@
+package com.frogobox.appkeyboard
+
+import com.frogobox.appkeyboard.ui.test.TypingMetricsCalculator
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+/**
+ * Unit tests for TypingMetricsCalculator domain logic
+ * Verifies character counting, word counting, line counting, and WPM calculation safety guards.
+ */
+class TypingMetricsTest {
+
+ @Test
+ fun testEmptyTextMetrics() {
+ val metrics = TypingMetricsCalculator.calculate("", 5000L)
+ assertEquals(0, metrics.characterCount)
+ assertEquals(0, metrics.wordCount)
+ assertEquals(0, metrics.lineCount)
+ assertEquals(0, metrics.wordsPerMinute)
+ assertEquals(5L, metrics.elapsedSeconds)
+ }
+
+ @Test
+ fun testWhitespaceOnlyMetrics() {
+ val metrics = TypingMetricsCalculator.calculate(" \n\t ", 10000L)
+ assertEquals(7, metrics.characterCount)
+ assertEquals(0, metrics.wordCount)
+ assertEquals(2, metrics.lineCount)
+ assertEquals(0, metrics.wordsPerMinute)
+ }
+
+ @Test
+ fun testEnglishPangramWordCount() {
+ val pangram = "The quick brown fox jumps over the lazy dog"
+ val metrics = TypingMetricsCalculator.calculate(pangram, 12000L)
+ assertEquals(43, metrics.characterCount)
+ assertEquals(9, metrics.wordCount)
+ assertEquals(1, metrics.lineCount)
+ // 9 words in 12s = 9 / (12/60) = 45 WPM
+ assertEquals(45, metrics.wordsPerMinute)
+ }
+
+ @Test
+ fun testIndonesianPangramMetrics() {
+ val pangram = "Keluarga Jefri bernomor fax tujuh belas"
+ val metrics = TypingMetricsCalculator.calculate(pangram, 6000L)
+ assertEquals(39, metrics.characterCount)
+ assertEquals(6, metrics.wordCount)
+ assertEquals(1, metrics.lineCount)
+ // 6 words in 6s = 6 / 0.1 = 60 WPM
+ assertEquals(60, metrics.wordsPerMinute)
+ }
+
+ @Test
+ fun testMultilineCount() {
+ val text = "Line 1\nLine 2\nLine 3\nLine 4"
+ val metrics = TypingMetricsCalculator.calculate(text, 20000L)
+ assertEquals(4, metrics.lineCount)
+ assertEquals(8, metrics.wordCount)
+ }
+
+ @Test
+ fun testWpmShortDurationReturnsZero() {
+ val text = "Hello world quick test"
+ val metrics = TypingMetricsCalculator.calculate(text, 1500L) // 1.5s < 2s threshold
+ assertEquals(4, metrics.wordCount)
+ assertEquals(0, metrics.wordsPerMinute)
+ }
+
+ @Test
+ fun testWpmClampedToMax300() {
+ // 500 words pasted in 3 seconds -> extreme raw WPM
+ val manyWords = (1..500).joinToString(" ") { "word$it" }
+ val metrics = TypingMetricsCalculator.calculate(manyWords, 3000L)
+ assertEquals(500, metrics.wordCount)
+ assertTrue(metrics.wordsPerMinute <= 300)
+ assertEquals(300, metrics.wordsPerMinute)
+ }
+}
diff --git a/app/src/test/java/com/frogobox/appkeyboard/WordSuggestionEngineTest.kt b/app/src/test/java/com/frogobox/appkeyboard/WordSuggestionEngineTest.kt
new file mode 100644
index 0000000..eb9440f
--- /dev/null
+++ b/app/src/test/java/com/frogobox/appkeyboard/WordSuggestionEngineTest.kt
@@ -0,0 +1,114 @@
+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
+import org.junit.Test
+
+/**
+ * Unit test suite for WordSuggestionEngine
+ * Validates three-section candidates (Issue #50):
+ * - Section 1: user's literal word
+ * - Section 2: predicted word
+ * - Section 3: auto-correct word
+ */
+class WordSuggestionEngineTest {
+
+ private lateinit var engine: WordSuggestionEngine
+
+ @Before
+ fun setup() {
+ engine = WordSuggestionEngine()
+ }
+
+ @Test
+ fun testThreeSectionIntegrity() {
+ val result = engine.getSuggestions("th")
+ assertNotNull(result)
+ assertEquals("th", result.userWord)
+ assertTrue(result.predictedWord.isNotEmpty())
+ assertTrue(result.autoCorrectWord.isNotEmpty())
+ assertTrue(result.hasSuggestions())
+ }
+
+ @Test
+ fun testPrefixPrediction() {
+ val result = engine.getSuggestions("appl")
+ assertEquals("appl", result.userWord)
+ assertEquals("apple", result.predictedWord.lowercase())
+ }
+
+ @Test
+ fun testCommonTypoAutoCorrection() {
+ // Test "teh" -> "the"
+ val resultThe = engine.getSuggestions("teh")
+ assertEquals("teh", resultThe.userWord)
+ assertEquals("the", resultThe.predictedWord.lowercase())
+
+ // Test "wht" -> "what"
+ val resultWhat = engine.getSuggestions("wht")
+ assertEquals("wht", resultWhat.userWord)
+ assertEquals("what", resultWhat.predictedWord.lowercase())
+
+ // Test "helo" -> "hello"
+ val resultHello = engine.getSuggestions("helo")
+ assertEquals("helo", resultHello.userWord)
+ assertEquals("hello", resultHello.predictedWord.lowercase())
+ }
+
+ @Test
+ fun testCasingPreservationLowercase() {
+ val result = engine.getSuggestions("appl")
+ assertEquals("appl", result.userWord)
+ assertEquals("apple", result.predictedWord)
+ }
+
+ @Test
+ fun testCasingPreservationTitleCase() {
+ val result = engine.getSuggestions("Appl")
+ assertEquals("Appl", result.userWord)
+ assertEquals("Apple", result.predictedWord)
+ }
+
+ @Test
+ fun testCasingPreservationUppercase() {
+ val result = engine.getSuggestions("APPL")
+ assertEquals("APPL", result.userWord)
+ assertEquals("APPLE", result.predictedWord)
+ }
+
+ @Test
+ fun testLevenshteinDistanceCalculation() {
+ assertEquals(0, engine.calculateLevenshteinDistance("test", "test"))
+ assertEquals(1, engine.calculateLevenshteinDistance("hello", "helo"))
+ assertEquals(1, engine.calculateLevenshteinDistance("apply", "apple"))
+ assertEquals(3, engine.calculateLevenshteinDistance("kitten", "sitting"))
+ }
+
+ @Test
+ fun testUserCustomWordLearning() {
+ engine.addUserWord("FrogoDeveloper")
+ val result = engine.getSuggestions("frogodev")
+ assertEquals("frogodev", result.userWord)
+ assertEquals("frogodeveloper", result.predictedWord.lowercase())
+ }
+
+ @Test
+ fun testEmptyInputHandling() {
+ val result = engine.getSuggestions("")
+ assertNotNull(result)
+ assertEquals("", result.userWord)
+ assertTrue(result.predictedWord.isNotEmpty())
+ }
+
+ @Test
+ fun testBlankWhitespaceInputHandling() {
+ val result = engine.getSuggestions(" ")
+ assertNotNull(result)
+ assertEquals("", result.userWord)
+ }
+
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index 47d56ba..2d79dba 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -2,12 +2,13 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
- alias(libs.plugins.jetbrains.kotlin.android) apply false
alias(libs.plugins.jetbrains.kotlin.jvm) apply false
+ alias(libs.plugins.jetbrains.kotlin.compose) apply false
alias(libs.plugins.hilt) apply false
alias(libs.plugins.ksp) apply false
}
tasks.register("clean", Delete::class) {
+ description = ""
delete(rootProject.layout.buildDirectory)
}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/ProjectSetting.kt b/buildSrc/src/main/kotlin/ProjectSetting.kt
index 337ef85..5e783c2 100644
--- a/buildSrc/src/main/kotlin/ProjectSetting.kt
+++ b/buildSrc/src/main/kotlin/ProjectSetting.kt
@@ -27,9 +27,9 @@ object ProjectSetting {
// ---------------------------------------------------------------------------------------------
- const val PROJECT_MIN_SDK = 24
- const val PROJECT_COMPILE_SDK = 36
- const val PROJECT_TARGET_SDK = PROJECT_COMPILE_SDK
+ const val PROJECT_MIN_SDK = 25
+ const val PROJECT_COMPILE_SDK = 37
+ const val PROJECT_TARGET_SDK = 36
// ---------------------------------------------------------------------------------------------
diff --git a/frogo-keyboard/build.gradle.kts b/frogo-keyboard/build.gradle.kts
index 28265bd..c8439dd 100644
--- a/frogo-keyboard/build.gradle.kts
+++ b/frogo-keyboard/build.gradle.kts
@@ -2,14 +2,13 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.library)
- alias(libs.plugins.jetbrains.kotlin.android)
`maven-publish`
}
android {
namespace = ProjectSetting.PROJECT_NAME_SPACE_LIB_FROGO_KEYBOARD
- compileSdk = ProjectSetting.PROJECT_TARGET_SDK
+ compileSdk = ProjectSetting.PROJECT_COMPILE_SDK
defaultConfig {
@@ -40,6 +39,13 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
+ publishing {
+ singleVariant("release") {
+ withSourcesJar()
+ withJavadocJar()
+ }
+ }
+
}
kotlin {
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 54429fd..16aea89 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
@@ -27,13 +27,15 @@ 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 {
- // how quickly do we have to doubletap shift to enable permanent caps lock
- var SHIFT_PERM_TOGGLE_SPEED = 500
- val KEYBOARD_LETTERS = 0
- val KEYBOARD_SYMBOLS = 1
- val KEYBOARD_SYMBOLS_SHIFT = 2
- val KEYBOARD_NUMBER = 3
- val KEYCODE_EMOJI = -6
+ companion object {
+ // How quickly do we have to doubletap shift to enable permanent caps lock
+ const val SHIFT_PERM_TOGGLE_SPEED = 500L
+
+ const val KEYBOARD_LETTERS = 0
+ const val KEYBOARD_SYMBOLS = 1
+ const val KEYBOARD_SYMBOLS_SHIFT = 2
+ const val KEYBOARD_NUMBER = 3
+ }
var keyboard: ItemMainKeyboard? = null
@@ -77,12 +79,12 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo
}
override fun onPress(primaryCode: Int) {
-
}
override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) {
super.onStartInput(attribute, restarting)
- inputTypeClass = attribute!!.inputType and TYPE_MASK_CLASS
+ if (attribute == null) return
+ inputTypeClass = attribute.inputType and TYPE_MASK_CLASS
enterKeyType = attribute.imeOptions and (IME_MASK_ACTION or IME_FLAG_NO_ENTER_ACTION)
val keyboardXml = when (inputTypeClass) {
@@ -107,9 +109,8 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo
updateShiftKeyState()
}
-
override fun onKey(code: Int) {
- var inputConnection = currentInputConnection
+ val inputConnection = currentInputConnection ?: return
onKeyExt(code, inputConnection)
}
@@ -120,7 +121,7 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo
val editorInfo = currentInputEditorInfo
if (editorInfo != null && editorInfo.inputType != InputType.TYPE_NULL && keyboard?.mShiftState != SHIFT_ON_PERMANENT) {
- if (currentInputConnection.getCursorCapsMode(editorInfo.inputType) != 0) {
+ if (currentInputConnection?.getCursorCapsMode(editorInfo.inputType) != 0) {
keyboard?.setShifted(SHIFT_ON_ONE_CHAR)
}
}
@@ -139,7 +140,7 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo
}
override fun onText(text: String) {
- currentInputConnection?.commitText(text, 0)
+ currentInputConnection?.commitText(text, 1)
}
override fun initialSetupKeyboard() {}
@@ -193,7 +194,7 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo
if (keyboardMode == KEYBOARD_LETTERS) {
val editorInfo = currentInputEditorInfo
if (editorInfo != null && editorInfo.inputType != InputType.TYPE_NULL && keyboard?.mShiftState != SHIFT_ON_PERMANENT) {
- if (currentInputConnection.getCursorCapsMode(editorInfo.inputType) != 0) {
+ if (currentInputConnection?.getCursorCapsMode(editorInfo.inputType) != 0) {
keyboard?.setShifted(SHIFT_ON_ONE_CHAR)
invalidateAllKeys()
}
@@ -201,11 +202,8 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo
}
}
-
override fun onKeyExt(code: Int, inputConnection: InputConnection) {
- if (keyboard == null) {
- return
- }
+ val kb = keyboard ?: return
if (code != ItemMainKeyboard.KEYCODE_SHIFT) {
lastShiftPressTS = 0
@@ -213,19 +211,18 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo
when (code) {
ItemMainKeyboard.KEYCODE_DELETE -> {
- if (keyboard!!.mShiftState == SHIFT_ON_ONE_CHAR) {
- keyboard!!.mShiftState = SHIFT_OFF
+ if (kb.mShiftState == SHIFT_ON_ONE_CHAR) {
+ kb.mShiftState = SHIFT_OFF
}
val selectedText = inputConnection.getSelectedText(0)
if (TextUtils.isEmpty(selectedText)) {
inputConnection.sendKeyEvent(
- KeyEvent(
- KeyEvent.ACTION_DOWN,
- KeyEvent.KEYCODE_DEL
- )
+ KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)
+ )
+ inputConnection.sendKeyEvent(
+ KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL)
)
- inputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL))
} else {
inputConnection.commitText("", 1)
}
@@ -239,14 +236,10 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo
ItemMainKeyboard.KEYCODE_SHIFT -> {
if (keyboardMode == KEYBOARD_LETTERS) {
when {
- keyboard!!.mShiftState == SHIFT_ON_PERMANENT -> keyboard!!.mShiftState =
- SHIFT_OFF
- System.currentTimeMillis() - lastShiftPressTS < SHIFT_PERM_TOGGLE_SPEED -> keyboard!!.mShiftState =
- SHIFT_ON_PERMANENT
- keyboard!!.mShiftState == SHIFT_ON_ONE_CHAR -> keyboard!!.mShiftState =
- SHIFT_OFF
- keyboard!!.mShiftState == SHIFT_OFF -> keyboard!!.mShiftState =
- SHIFT_ON_ONE_CHAR
+ kb.mShiftState == SHIFT_ON_PERMANENT -> kb.mShiftState = SHIFT_OFF
+ System.currentTimeMillis() - lastShiftPressTS < SHIFT_PERM_TOGGLE_SPEED -> kb.mShiftState = SHIFT_ON_PERMANENT
+ kb.mShiftState == SHIFT_ON_ONE_CHAR -> kb.mShiftState = SHIFT_OFF
+ kb.mShiftState == SHIFT_OFF -> kb.mShiftState = SHIFT_ON_ONE_CHAR
}
lastShiftPressTS = System.currentTimeMillis()
@@ -269,23 +262,16 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo
inputConnection.performEditorAction(imeOptionsActionId)
} else {
inputConnection.sendKeyEvent(
- KeyEvent(
- KeyEvent.ACTION_DOWN,
- KeyEvent.KEYCODE_ENTER
- )
+ KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER)
)
inputConnection.sendKeyEvent(
- KeyEvent(
- KeyEvent.ACTION_UP,
- KeyEvent.KEYCODE_ENTER
- )
+ KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER)
)
}
if (inputConnection != currentInputConnection) {
inputConnection.commitText("\n", 1)
}
-
}
ItemMainKeyboard.KEYCODE_MODE_CHANGE -> {
val keyboardXml = if (keyboardMode == KEYBOARD_LETTERS) {
@@ -301,9 +287,45 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo
ItemMainKeyboard.KEYCODE_EMOJI -> {
runEmojiBoard()
}
+ ItemMainKeyboard.KEYCODE_TAB -> {
+ val isMultiline = (currentInputEditorInfo?.inputType ?: 0) and InputType.TYPE_TEXT_FLAG_MULTI_LINE != 0
+ if (isMultiline) {
+ inputConnection.commitText("\t", 1)
+ } else {
+ inputConnection.sendKeyEvent(
+ KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB)
+ )
+ inputConnection.sendKeyEvent(
+ KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_TAB)
+ )
+ }
+ }
+ ItemMainKeyboard.KEYCODE_ARROW_LEFT -> {
+ moveCursor(false)
+ }
+ ItemMainKeyboard.KEYCODE_ARROW_RIGHT -> {
+ moveCursor(true)
+ }
+ ItemMainKeyboard.KEYCODE_ARROW_UP -> {
+ inputConnection.sendKeyEvent(
+ KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_UP)
+ )
+ inputConnection.sendKeyEvent(
+ KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_UP)
+ )
+ }
+ ItemMainKeyboard.KEYCODE_ARROW_DOWN -> {
+ inputConnection.sendKeyEvent(
+ KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN)
+ )
+ inputConnection.sendKeyEvent(
+ KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_DOWN)
+ )
+ }
else -> {
+ if (code <= 0) return
var codeChar = code.toChar()
- if (Character.isLetter(codeChar) && keyboard!!.mShiftState > SHIFT_OFF) {
+ if (Character.isLetter(codeChar) && kb.mShiftState > SHIFT_OFF) {
codeChar = Character.toUpperCase(codeChar)
}
@@ -314,14 +336,14 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo
val originalText =
inputConnection.getExtractedText(ExtractedTextRequest(), 0)?.text ?: return
inputConnection.commitText(codeChar.toString(), 1)
- val newText = inputConnection.getExtractedText(ExtractedTextRequest(), 0).text
- switchToLetters = originalText != newText
+ val newText = inputConnection.getExtractedText(ExtractedTextRequest(), 0)?.text
+ switchToLetters = newText != null && originalText != newText
} else {
inputConnection.commitText(codeChar.toString(), 1)
}
- if (keyboard!!.mShiftState == SHIFT_ON_ONE_CHAR && keyboardMode == KEYBOARD_LETTERS) {
- keyboard!!.mShiftState = SHIFT_OFF
+ if (kb.mShiftState == SHIFT_ON_ONE_CHAR && keyboardMode == KEYBOARD_LETTERS) {
+ kb.mShiftState = SHIFT_OFF
invalidateAllKeys()
}
}
@@ -336,10 +358,11 @@ abstract class BaseKeyboardIME : InputMethodService(), OnKeybo
val extractedText =
currentInputConnection?.getExtractedText(ExtractedTextRequest(), 0) ?: return
var newCursorPosition = extractedText.selectionStart
+ val textLength = extractedText.text?.length ?: return
newCursorPosition = if (moveRight) {
- newCursorPosition + 1
+ (newCursorPosition + 1).coerceAtMost(textLength)
} else {
- newCursorPosition - 1
+ (newCursorPosition - 1).coerceAtLeast(0)
}
currentInputConnection?.setSelection(newCursorPosition, newCursorPosition)
diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/ext/ContextExt.kt b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/ext/ContextExt.kt
index 85576ae..d00900e 100644
--- a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/ext/ContextExt.kt
+++ b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/ext/ContextExt.kt
@@ -3,11 +3,10 @@ package com.frogobox.libkeyboard.common.ext
import android.content.Context
import android.content.res.Configuration
import android.content.res.Configuration.UI_MODE_NIGHT_YES
-import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
import android.widget.ImageView
-import com.frogobox.libkeyboard.R
-import com.frogobox.sdk.ext.getColorExt
+import androidx.core.graphics.BlendModeColorFilterCompat
+import androidx.core.graphics.BlendModeCompat
/**
* Created by Faisal Amir on 24/10/22
@@ -23,6 +22,11 @@ fun Context.isDarkThemeOn(): Boolean {
return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
}
-fun Drawable.applyColorFilter(color: Int) = mutate().setColorFilter(color, PorterDuff.Mode.SRC_IN)
+fun Drawable.applyColorFilter(color: Int) {
+ mutate()
+ colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_IN)
+}
-fun ImageView.applyColorFilter(color: Int) = setColorFilter(color, PorterDuff.Mode.SRC_IN)
\ No newline at end of file
+fun ImageView.applyColorFilter(color: Int) {
+ colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_IN)
+}
\ No newline at end of file
diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/ext/IntExt.kt b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/ext/IntExt.kt
index 0ecf452..bdb1602 100644
--- a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/ext/IntExt.kt
+++ b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/ext/IntExt.kt
@@ -1,13 +1,19 @@
package com.frogobox.libkeyboard.common.ext
import android.graphics.Color
+import kotlin.math.roundToInt
fun hsl2hsv(hsl: FloatArray): FloatArray {
val hue = hsl[0]
var sat = hsl[1]
val light = hsl[2]
sat *= if (light < .5) light else 1 - light
- return floatArrayOf(hue, 2f * sat / (light + sat), light + sat)
+ val denominator = light + sat
+ return if (denominator == 0f) {
+ floatArrayOf(hue, 0f, 0f)
+ } else {
+ floatArrayOf(hue, 2f * sat / denominator, denominator)
+ }
}
fun hsv2hsl(hsv: FloatArray): FloatArray {
@@ -16,45 +22,37 @@ fun hsv2hsl(hsv: FloatArray): FloatArray {
val value = hsv[2]
val newHue = (2f - sat) * value
- var newSat = sat * value / if (newHue < 1f) newHue else 2f - newHue
+ val denominator = if (newHue < 1f) newHue else 2f - newHue
+ var newSat = if (denominator == 0f) 0f else sat * value / denominator
if (newSat > 1f) newSat = 1f
return floatArrayOf(hue, newSat, newHue / 2f)
}
-fun Int.lightenColor(factor: Int = 8): Int {
+/**
+ * Adjusts the lightness of a color by a given factor (in percent).
+ * Positive factor lightens, negative factor darkens.
+ */
+private fun Int.adjustLightness(factor: Int): Int {
if (this == Color.WHITE || this == Color.BLACK) {
return this
}
- val LIGHT_FACTOR = factor
var hsv = FloatArray(3)
Color.colorToHSV(this, hsv)
val hsl = hsv2hsl(hsv)
- hsl[2] += LIGHT_FACTOR / 100f
- if (hsl[2] < 0) hsl[2] = 0f
+ hsl[2] = (hsl[2] + factor / 100f).coerceIn(0f, 1f)
hsv = hsl2hsv(hsl)
return Color.HSVToColor(hsv)
}
-// taken from https://stackoverflow.com/a/40964456/1967672
-fun Int.darkenColor(factor: Int = 8): Int {
- if (this == Color.WHITE || this == Color.BLACK) {
- return this
- }
+fun Int.lightenColor(factor: Int = 8): Int = adjustLightness(factor)
- val DARK_FACTOR = factor
- var hsv = FloatArray(3)
- Color.colorToHSV(this, hsv)
- val hsl = hsv2hsl(hsv)
- hsl[2] -= DARK_FACTOR / 100f
- if (hsl[2] < 0) hsl[2] = 0f
- hsv = hsl2hsv(hsl)
- return Color.HSVToColor(hsv)
-}
+// taken from https://stackoverflow.com/a/40964456/1967672
+fun Int.darkenColor(factor: Int = 8): Int = adjustLightness(-factor)
fun Int.adjustAlpha(factor: Float): Int {
- val alpha = Math.round(Color.alpha(this) * factor)
+ val alpha = (Color.alpha(this) * factor).roundToInt()
val red = Color.red(this)
val green = Color.green(this)
val blue = Color.blue(this)
@@ -66,4 +64,3 @@ fun Int.getContrastColor(): Int {
val y = (299 * Color.red(this) + 587 * Color.green(this) + 114 * Color.blue(this)) / 1000
return if (y >= 149 && this != Color.BLACK) DARK_GREY else Color.WHITE
}
-
diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/sound/MechanicalSoundManager.kt b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/sound/MechanicalSoundManager.kt
new file mode 100644
index 0000000..a90a937
--- /dev/null
+++ b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/common/sound/MechanicalSoundManager.kt
@@ -0,0 +1,129 @@
+package com.frogobox.libkeyboard.common.sound
+
+import android.content.Context
+import android.media.AudioAttributes
+import android.media.AudioManager
+import android.media.SoundPool
+import android.util.Log
+import androidx.annotation.RawRes
+import com.frogobox.libkeyboard.R
+import java.util.concurrent.ConcurrentHashMap
+import kotlin.random.Random
+
+/**
+ * Mechanical Keyboard Switch Profiles (Mechvibes compatible)
+ */
+enum class MechanicalSoundType(
+ val id: String,
+ val title: String,
+ val description: String,
+ @RawRes val rawResId: Int
+) {
+ OFF("off", "Off (Mute)", "Silent typing with no acoustic feedback", 0),
+ SYSTEM_CLICK("system", "System Default Click", "Native Android system keyclick sound effect", -1),
+ CHERRY_MX_BLUE("cherry_mx_blue", "Cherry MX Blue", "Crisp tactile click with high-frequency snap", R.raw.mechanical_cherry_blue),
+ CHERRY_MX_BROWN("cherry_mx_brown", "Cherry MX Brown", "Balanced tactile bump with rounded acoustic thock", R.raw.mechanical_cherry_brown),
+ CHERRY_MX_RED("cherry_mx_red", "Cherry MX Red", "Smooth linear travel with clean bottom-out thock", R.raw.mechanical_cherry_red),
+ TYPEWRITER("typewriter", "Classic Typewriter", "Vintage mechanical typewriter strike & carriage clink", R.raw.mechanical_typewriter);
+
+ companion object {
+ fun fromId(id: String?): MechanicalSoundType {
+ return entries.firstOrNull { it.id.equals(id, ignoreCase = true) } ?: CHERRY_MX_BLUE
+ }
+ }
+}
+
+/**
+ * Low-latency SoundPool Audio Engine for Mechanical Keyboard feedback.
+ */
+class MechanicalSoundManager private constructor(context: Context) {
+
+ companion object {
+ private const val TAG = "MechanicalSoundManager"
+ const val PREF_KEYBOARD_SOUND_ENABLED = "KEYBOARD_SOUND_ENABLED"
+ const val PREF_KEYBOARD_SOUND_TYPE = "KEYBOARD_SOUND_TYPE"
+ const val PREF_KEYBOARD_SOUND_VOLUME = "KEYBOARD_SOUND_VOLUME"
+ const val PREF_KEYBOARD_VIBRATE_ENABLED = "KEYBOARD_VIBRATE_ENABLED"
+
+ @Volatile
+ private var INSTANCE: MechanicalSoundManager? = null
+
+ fun getInstance(context: Context): MechanicalSoundManager {
+ return INSTANCE ?: synchronized(this) {
+ INSTANCE ?: MechanicalSoundManager(context.applicationContext).also { INSTANCE = it }
+ }
+ }
+ }
+
+ private val appContext = context.applicationContext
+ private val audioManager = appContext.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
+ private var soundPool: SoundPool? = null
+ private val soundIdMap = ConcurrentHashMap()
+ private val loadedSounds = ConcurrentHashMap()
+
+ init {
+ initSoundPool()
+ }
+
+ private fun initSoundPool() {
+ val audioAttributes = AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .build()
+
+ soundPool = SoundPool.Builder()
+ .setMaxStreams(8)
+ .setAudioAttributes(audioAttributes)
+ .build().apply {
+ setOnLoadCompleteListener { _, sampleId, status ->
+ if (status == 0) {
+ loadedSounds[sampleId] = true
+ }
+ }
+ }
+
+ preloadSounds()
+ }
+
+ private fun preloadSounds() {
+ val pool = soundPool ?: return
+ for (type in MechanicalSoundType.entries) {
+ if (type.rawResId > 0) {
+ try {
+ val soundId = pool.load(appContext, type.rawResId, 1)
+ soundIdMap[type] = soundId
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to load sound buffer for $type", e)
+ }
+ }
+ }
+ }
+
+ fun playKeySound(
+ soundType: MechanicalSoundType = MechanicalSoundType.CHERRY_MX_BLUE,
+ volume: Float = 0.8f
+ ) {
+ if (soundType == MechanicalSoundType.OFF) return
+
+ if (soundType == MechanicalSoundType.SYSTEM_CLICK) {
+ audioManager?.playSoundEffect(AudioManager.FX_KEYPRESS_STANDARD, volume)
+ return
+ }
+
+ val pool = soundPool ?: return
+ val soundId = soundIdMap[soundType] ?: return
+
+ // Slight human acoustic jitter (+/- 2% pitch variation)
+ val pitchVariation = 0.98f + Random.nextFloat() * 0.04f
+ val clampedVol = volume.coerceIn(0.05f, 1.0f)
+
+ pool.play(soundId, clampedVol, clampedVol, 1, 0, pitchVariation)
+ }
+
+ fun release() {
+ soundPool?.release()
+ soundPool = null
+ soundIdMap.clear()
+ loadedSounds.clear()
+ }
+}
diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiHelper.kt b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiHelper.kt
index f9c9a3f..878fef5 100644
--- a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiHelper.kt
+++ b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/emoji/EmojiHelper.kt
@@ -1,9 +1,12 @@
package com.frogobox.libkeyboard.ui.emoji
import android.content.Context
+import android.util.Log
+
+private const val TAG = "EmojiHelper"
/**
- * Reads the emoji list at the given [path] and returns an parsed [MutableList]. If the
+ * Reads the emoji list at the given [path] and returns a parsed [MutableList]. If the
* given file path does not exist, an empty [MutableList] is returned.
*
* @param context The initiating view's context.
@@ -17,54 +20,52 @@ fun parseRawEmojiSpecsFile(context: Context, path: String): MutableList
fun commitEmojiEditorList() {
emojiEditorList?.let {
// add only the base emoji for now, ignore the variations
- emojis.add(it.first())
+ if (it.isNotEmpty()) {
+ emojis.add(it.first())
+ }
}
emojiEditorList = null
}
- context.assets.open(path).bufferedReader().useLines { lines ->
- for (line in lines) {
- if (line.startsWith("#")) {
- // Comment line
- } else if (line.startsWith("[")) {
- commitEmojiEditorList()
- } else if (line.trim().isEmpty()) {
- // Empty line
- continue
- } else {
- if (!line.startsWith("\t")) {
+ try {
+ context.assets.open(path).bufferedReader().useLines { lines ->
+ for (line in lines) {
+ if (line.startsWith("#")) {
+ // Comment line
+ } else if (line.startsWith("[")) {
commitEmojiEditorList()
- }
+ } else if (line.trim().isEmpty()) {
+ // Empty line
+ continue
+ } else {
+ if (!line.startsWith("\t")) {
+ commitEmojiEditorList()
+ }
- // Assume it is a data line
- val data = line.split(";")
- if (data.size == 3) {
- val emoji = data[0].trim()
- if (emojiEditorList != null) {
- emojiEditorList!!.add(emoji)
- } else {
- emojiEditorList = mutableListOf(emoji)
+ // Assume it is a data line
+ val data = line.split(";")
+ if (data.size == 3) {
+ val emoji = data[0].trim()
+ emojiEditorList?.add(emoji) ?: run {
+ emojiEditorList = mutableListOf(emoji)
+ }
}
}
}
+ commitEmojiEditorList()
}
- commitEmojiEditorList()
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to parse emoji file: $path", e)
}
-
+
return emojis
}
-fun getEmojiCategory() : List {
- return listOf(
- EmojiCategory(EmojiCategoryType.GENERAL.name, EmojiCategoryType.GENERAL.icon, EmojiCategoryType.GENERAL.path),
- EmojiCategory(EmojiCategoryType.SMILEYS_EMOTION.name, EmojiCategoryType.SMILEYS_EMOTION.icon, EmojiCategoryType.SMILEYS_EMOTION.path),
- EmojiCategory(EmojiCategoryType.PEOPLE_BODY.name, EmojiCategoryType.PEOPLE_BODY.icon, EmojiCategoryType.PEOPLE_BODY.path),
- EmojiCategory(EmojiCategoryType.ACTIVITIES.name, EmojiCategoryType.ACTIVITIES.icon, EmojiCategoryType.ACTIVITIES.path),
- EmojiCategory(EmojiCategoryType.ANIMAL_NATURE.name, EmojiCategoryType.ANIMAL_NATURE.icon, EmojiCategoryType.ANIMAL_NATURE.path),
- EmojiCategory(EmojiCategoryType.FOOD_DRINK.name, EmojiCategoryType.FOOD_DRINK.icon, EmojiCategoryType.FOOD_DRINK.path),
- EmojiCategory(EmojiCategoryType.FLAG.name, EmojiCategoryType.FLAG.icon, EmojiCategoryType.FLAG.path),
- EmojiCategory(EmojiCategoryType.OBJECTS.name, EmojiCategoryType.OBJECTS.icon, EmojiCategoryType.OBJECTS.path),
- EmojiCategory(EmojiCategoryType.TRAVEL_PLACES.name, EmojiCategoryType.TRAVEL_PLACES.icon, EmojiCategoryType.TRAVEL_PLACES.path),
- EmojiCategory(EmojiCategoryType.SYMBOLS.name, EmojiCategoryType.SYMBOLS.icon, EmojiCategoryType.SYMBOLS.path)
- )
+/**
+ * Returns all emoji categories auto-generated from [EmojiCategoryType] enum entries.
+ */
+fun getEmojiCategory(): List {
+ return EmojiCategoryType.entries.map { type ->
+ EmojiCategory(name = type.name, icon = type.icon, path = type.path)
+ }
}
\ No newline at end of file
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 5671c71..ed19597 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
@@ -36,6 +36,14 @@ class EmojiKeyboard(
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 {
@@ -50,6 +58,11 @@ class EmojiKeyboard(
setupEmojiCategory()
}
+ /** Clears the emoji cache. Call this if emoji assets are updated. */
+ fun clearEmojiCache() {
+ emojiCache.clear()
+ }
+
private fun setupEmojiCategory() {
val adapterCallback =
object : IFrogoBindingAdapter {
@@ -106,11 +119,15 @@ class EmojiKeyboard(
}
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 systemFontPaint = Paint().apply {
- typeface = Typeface.DEFAULT
- }
val emojis = fullEmojiList.filter { emoji ->
systemFontPaint.hasGlyph(emoji) || (EmojiCompat.get().loadState == EmojiCompat.LOAD_STATE_SUCCEEDED
@@ -120,10 +137,12 @@ class EmojiKeyboard(
) == EmojiCompat.EMOJI_SUPPORTED)
}
+ // Store in cache
+ emojiCache[path] = emojis
+
Handler(Looper.getMainLooper()).post {
setupEmojiAdapter(emojis)
}
-
}
}
@@ -136,7 +155,7 @@ class EmojiKeyboard(
position: Int,
notifyListener: FrogoRecyclerNotifyListener
) {
- mOnKeyboardActionListener!!.onText(data)
+ mOnKeyboardActionListener?.onText(data)
}
override fun onItemLongClicked(
@@ -145,7 +164,7 @@ class EmojiKeyboard(
position: Int,
notifyListener: FrogoRecyclerNotifyListener
) {
- mOnKeyboardActionListener!!.onText(data)
+ mOnKeyboardActionListener?.onText(data)
}
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
@@ -153,7 +172,7 @@ class EmojiKeyboard(
}
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
- return oldItem.hashCode() == newItem.hashCode()
+ return oldItem == newItem
}
override fun setViewBinding(parent: ViewGroup): ItemKeyboardEmojiBinding {
diff --git a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/ItemMainKeyboard.kt b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/ItemMainKeyboard.kt
index a82c06b..7b3099f 100644
--- a/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/ItemMainKeyboard.kt
+++ b/frogo-keyboard/src/main/java/com/frogobox/libkeyboard/ui/main/ItemMainKeyboard.kt
@@ -6,6 +6,7 @@ import android.content.res.Resources
import android.content.res.TypedArray
import android.content.res.XmlResourceParser
import android.graphics.drawable.Drawable
+import android.util.Log
import android.util.TypedValue
import android.util.Xml
import android.view.inputmethod.EditorInfo
@@ -42,7 +43,7 @@ class ItemMainKeyboard {
var mMinWidth = 0
/** List of keys in this keyboard */
- var mKeys: MutableList? = null
+ val mKeys = mutableListOf()
/** Width of the screen available to fit the keyboard */
private var mDisplayWidth = 0
@@ -51,9 +52,10 @@ class ItemMainKeyboard {
private var mEnterKeyType = IME_ACTION_NONE
/** Keyboard rows */
- private val mRows = ArrayList()
+ private val mRows = ArrayList()
companion object {
+ private const val TAG = "ItemMainKeyboard"
private const val TAG_KEYBOARD = "Keyboard"
private const val TAG_ROW = "Row"
private const val TAG_KEY = "Key"
@@ -66,8 +68,24 @@ class ItemMainKeyboard {
const val KEYCODE_DELETE = -5
const val KEYCODE_SPACE = 32
const val KEYCODE_EMOJI = -6
+ const val KEYCODE_TAB = -7
+ const val KEYCODE_ARROW_LEFT = -8
+ const val KEYCODE_ARROW_RIGHT = -9
+ const val KEYCODE_ARROW_UP = -10
+ const val KEYCODE_ARROW_DOWN = -11
+
+ /** Whether haptic feedback is enabled on key press. Set via [MainKeyboard.vibrateOnKeypress]. */
+ var VIBRATE_ON_KEYPRESS = true
+
+ /** Whether key sound feedback is enabled on key press. Set via [MainKeyboard.soundOnKeypress]. */
+ var SOUND_ON_KEYPRESS = true
+
+ /** Selected mechanical switch sound profile id. Default is Cherry MX Blue */
+ var MECHANICAL_SOUND_TYPE = "cherry_mx_blue"
+
+ /** Sound volume multiplier (0.05f to 1.0f) */
+ var SOUND_VOLUME = 0.8f
- const val VIBRATE_ON_KEYPRESS = false
const val SHOW_POPUP_ON_KEYPRESS = true
const val SHIFT_OFF = 0
@@ -85,10 +103,7 @@ class ItemMainKeyboard {
val value = a.peekValue(index) ?: return defValue
return when (value.type) {
TypedValue.TYPE_DIMENSION -> a.getDimensionPixelOffset(index, defValue)
- TypedValue.TYPE_FRACTION -> Math.round(a.getFraction(index,
- base,
- base,
- defValue.toFloat()))
+ TypedValue.TYPE_FRACTION -> a.getFraction(index, base, base, defValue.toFloat()).roundToInt()
else -> defValue
}
}
@@ -233,7 +248,9 @@ class ItemMainKeyboard {
repeatable = a.getBoolean(R.styleable.FrogoKeyboard_Key_isRepeatable, false)
edgeFlags = a.getInt(R.styleable.FrogoKeyboard_Key_keyEdgeFlags, 0)
icon = a.getDrawable(R.styleable.FrogoKeyboard_Key_keyIcon)
- icon?.setBounds(0, 0, icon!!.intrinsicWidth, icon!!.intrinsicHeight)
+ icon?.let { ic ->
+ ic.setBounds(0, 0, ic.intrinsicWidth, ic.intrinsicHeight)
+ }
label = a.getText(R.styleable.FrogoKeyboard_Key_keyLabel) ?: ""
topSmallNumber = a.getString(R.styleable.FrogoKeyboard_Key_topSmallNumber) ?: ""
@@ -261,10 +278,9 @@ class ItemMainKeyboard {
fun isInside(x: Int, y: Int): Boolean {
val leftEdge = edgeFlags and EDGE_LEFT > 0
val rightEdge = edgeFlags and EDGE_RIGHT > 0
- return ((x >= this.x || leftEdge && x <= this.x + width)
- && (x < this.x + width || rightEdge && x >= this.x)
- && (y >= this.y && y <= this.y + height)
- && (y < this.y + height && y >= this.y))
+ val isInsideX = (x >= this.x || leftEdge) && (x < this.x + width || rightEdge)
+ val isInsideY = y >= this.y && y < this.y + height
+ return isInsideX && isInsideY
}
}
@@ -280,7 +296,23 @@ class ItemMainKeyboard {
mDefaultWidth = mDisplayWidth / 10
mDefaultHeight = mDefaultWidth
mKeyboardHeightMultiplier = getKeyboardHeightMultiplier()
- mKeys = ArrayList()
+ mEnterKeyType = enterKeyType
+ loadKeyboard(context, context.resources.getXml(xmlLayoutResId))
+ }
+
+ /**
+ * Creates a keyboard from the given xml key layout file with a custom height multiplier.
+ * @param context the application or service context
+ * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
+ * @param enterKeyType determines what icon should we show on Enter key
+ * @param heightMultiplier multiplier for keyboard height (1.0 = default, 1.2 = 20% taller)
+ */
+ constructor(context: Context, @XmlRes xmlLayoutResId: Int, enterKeyType: Int, heightMultiplier: Float) {
+ mDisplayWidth = context.resources.displayMetrics.widthPixels
+ mDefaultHorizontalGap = 0
+ mDefaultWidth = mDisplayWidth / 10
+ mDefaultHeight = mDefaultWidth
+ mKeyboardHeightMultiplier = heightMultiplier
mEnterKeyType = enterKeyType
loadKeyboard(context, context.resources.getXml(xmlLayoutResId))
}
@@ -326,7 +358,7 @@ class ItemMainKeyboard {
key.code = character.code
column++
x += key.width + key.gap
- mKeys!!.add(key)
+ mKeys.add(key)
row.mKeys.add(key)
if (x > mMinWidth) {
mMinWidth = x
@@ -383,7 +415,7 @@ class ItemMainKeyboard {
TAG_KEY -> {
inKey = true
key = createKeyFromXml(res, currentRow!!, x, y, parser)
- mKeys!!.add(key)
+ mKeys.add(key)
if (key.code == KEYCODE_ENTER) {
val enterResourceId = when (mEnterKeyType) {
EditorInfo.IME_ACTION_SEARCH -> R.drawable.ic_keyboard_search
@@ -415,6 +447,7 @@ class ItemMainKeyboard {
}
}
} catch (e: Exception) {
+ Log.e(TAG, "Error loading keyboard XML", e)
}
mHeight = y
}
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 7192413..f265cfc 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
@@ -11,12 +11,15 @@ import android.graphics.PorterDuff
import android.graphics.Rect
import android.graphics.Typeface
import android.graphics.drawable.LayerDrawable
+import android.media.AudioManager
import android.os.Handler
+import android.os.Looper
import android.os.Message
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
@@ -29,12 +32,19 @@ import androidx.core.content.res.ResourcesCompat
import com.frogobox.libkeyboard.R
import com.frogobox.libkeyboard.common.ext.adjustAlpha
import com.frogobox.libkeyboard.common.ext.applyColorFilter
+import com.frogobox.libkeyboard.common.sound.MechanicalSoundManager
+import com.frogobox.libkeyboard.common.sound.MechanicalSoundType
+import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard.Companion.KEYCODE_ARROW_DOWN
+import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard.Companion.KEYCODE_ARROW_LEFT
+import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard.Companion.KEYCODE_ARROW_RIGHT
+import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard.Companion.KEYCODE_ARROW_UP
import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard.Companion.KEYCODE_DELETE
import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard.Companion.KEYCODE_EMOJI
import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard.Companion.KEYCODE_ENTER
import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard.Companion.KEYCODE_MODE_CHANGE
import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard.Companion.KEYCODE_SHIFT
import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard.Companion.KEYCODE_SPACE
+import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard.Companion.KEYCODE_TAB
import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard.Companion.MAX_KEYS_PER_MINI_ROW
import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard.Companion.SHIFT_OFF
import com.frogobox.libkeyboard.ui.main.ItemMainKeyboard.Companion.SHIFT_ON_ONE_CHAR
@@ -116,6 +126,22 @@ class MainKeyboard @JvmOverloads constructor(
private val mSpaceMoveThreshold: Int
private var ignoreTouches = false
+ /** Reusable Paint for top small numbers — avoids per-frame allocation */
+ private val mSmallLetterPaint = Paint()
+
+ /** Audio manager for key click sound feedback */
+ private val mAudioManager: AudioManager?
+
+ /** Whether haptic feedback is enabled — can be toggled at runtime */
+ var vibrateOnKeypress: Boolean
+ get() = ItemMainKeyboard.VIBRATE_ON_KEYPRESS
+ set(value) { ItemMainKeyboard.VIBRATE_ON_KEYPRESS = value }
+
+ /** Whether key click sound is enabled — can be toggled at runtime */
+ var soundOnKeypress: Boolean
+ get() = ItemMainKeyboard.SOUND_ON_KEYPRESS
+ set(value) { ItemMainKeyboard.SOUND_ON_KEYPRESS = value }
+
// For multi-tap
private var mLastTapTime = 0L
@@ -205,6 +231,7 @@ class MainKeyboard @JvmOverloads constructor(
mMiniKeyboardCache = HashMap()
mAccessibilityManager = (context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager)
+ mAudioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
mPopupMaxMoveDistance = resources.getDimension(R.dimen.popup_max_move_distance)
mTopSmallNumberSize = resources.getDimension(com.frogobox.ui.R.dimen.frogo_dimen_font_10sp)
@@ -212,14 +239,13 @@ class MainKeyboard @JvmOverloads constructor(
mTopSmallNumberMarginHeight = resources.getDimension(R.dimen.top_small_number_margin_height)
}
- @SuppressLint("HandlerLeak")
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (mHandler == null) {
- mHandler = object : Handler() {
+ mHandler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
when (msg.what) {
- MSG_REMOVE_PREVIEW -> mPreviewText!!.visibility = INVISIBLE
+ MSG_REMOVE_PREVIEW -> mPreviewText?.visibility = INVISIBLE
MSG_REPEAT -> if (repeatKey(false)) {
val repeat = Message.obtain(this, MSG_REPEAT)
sendMessageDelayed(repeat, REPEAT_INTERVAL.toLong())
@@ -267,8 +293,7 @@ class MainKeyboard @JvmOverloads constructor(
}
removeMessages()
mKeyboard = keyboard
- val keys = mKeyboard!!.mKeys
- mKeys = keys!!.toMutableList() as ArrayList
+ mKeys = ArrayList(keyboard.mKeys)
requestLayout()
mKeyboardChanged = true
invalidateAllKeys()
@@ -282,10 +307,15 @@ class MainKeyboard @JvmOverloads constructor(
fun vibrateIfNeeded() {
if (ItemMainKeyboard.VIBRATE_ON_KEYPRESS) {
- performHapticFeedback(
- HapticFeedbackConstants.VIRTUAL_KEY,
- HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
- )
+ performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
+ }
+ }
+
+ /** Plays mechanical keyboard sound if sound feedback is enabled. */
+ fun playSoundIfNeeded() {
+ if (ItemMainKeyboard.SOUND_ON_KEYPRESS) {
+ val soundType = MechanicalSoundType.fromId(ItemMainKeyboard.MECHANICAL_SOUND_TYPE)
+ MechanicalSoundManager.getInstance(context).playKeySound(soundType, ItemMainKeyboard.SOUND_VOLUME)
}
}
@@ -317,8 +347,9 @@ class MainKeyboard @JvmOverloads constructor(
}
private fun adjustCase(label: CharSequence): CharSequence? {
+ val kb = mKeyboard ?: return label
var newLabel: CharSequence? = label
- if (!newLabel.isNullOrEmpty() && mKeyboard!!.mShiftState > SHIFT_OFF && newLabel.length < 3 && Character.isLowerCase(
+ if (!newLabel.isNullOrEmpty() && kb.mShiftState > SHIFT_OFF && newLabel.length < 3 && Character.isLowerCase(
newLabel[0]
)
) {
@@ -328,14 +359,15 @@ class MainKeyboard @JvmOverloads constructor(
}
public override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- if (mKeyboard == null) {
+ val kb = mKeyboard
+ if (kb == null) {
setMeasuredDimension(0, 0)
} else {
- var width = mKeyboard!!.mMinWidth
+ var width = kb.mMinWidth
if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) {
width = MeasureSpec.getSize(widthMeasureSpec)
}
- setMeasuredDimension(width, mKeyboard!!.mHeight)
+ setMeasuredDimension(width, kb.mHeight)
}
}
@@ -397,12 +429,14 @@ class MainKeyboard @JvmOverloads constructor(
val paint = mPaint
val keys = mKeys
paint.color = mTextColor
- val smallLetterPaint = Paint().apply {
+ // 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)
@@ -416,7 +450,12 @@ class MainKeyboard @JvmOverloads constructor(
KEYCODE_SHIFT,
KEYCODE_DELETE,
KEYCODE_ENTER,
- KEYCODE_MODE_CHANGE -> {
+ KEYCODE_MODE_CHANGE,
+ KEYCODE_TAB,
+ KEYCODE_ARROW_LEFT,
+ KEYCODE_ARROW_RIGHT,
+ KEYCODE_ARROW_UP,
+ KEYCODE_ARROW_DOWN -> {
keyBackground = resources.getDrawable(R.drawable.keypad_action, context.theme)
}
@@ -468,8 +507,9 @@ class MainKeyboard @JvmOverloads constructor(
// Turn off drop shadow
paint.setShadowLayer(0f, 0f, 0f, 0)
} else if (key.icon != null && mKeyboard != null) {
+ val kb = mKeyboard!!
if (code == KEYCODE_SHIFT) {
- val drawableId = when (mKeyboard!!.mShiftState) {
+ 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
@@ -477,16 +517,20 @@ class MainKeyboard @JvmOverloads constructor(
key.icon = ResourcesCompat.getDrawable(resources, drawableId, null)
}
- if (code == KEYCODE_DELETE || code == KEYCODE_SHIFT || code == KEYCODE_EMOJI || code == KEYCODE_ENTER) {
- key.icon!!.applyColorFilter(mTextColor)
- }
+ 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 - key.icon!!.intrinsicWidth) / 2
- val drawableY = (key.height - key.icon!!.intrinsicHeight) / 2
- canvas.translate(drawableX.toFloat(), drawableY.toFloat())
- key.icon!!.setBounds(0, 0, key.icon!!.intrinsicWidth, key.icon!!.intrinsicHeight)
- key.icon!!.draw(canvas)
- canvas.translate(-drawableX.toFloat(), -drawableY.toFloat())
+ 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())
}
@@ -512,7 +556,9 @@ class MainKeyboard @JvmOverloads constructor(
if (index != NOT_A_KEY && index < mKeys.size) {
val key = mKeys[index]
getPressedKeyIndex(x, y)
- mOnKeyboardActionListener!!.onKey(key.code)
+ vibrateIfNeeded()
+ playSoundIfNeeded()
+ mOnKeyboardActionListener?.onKey(key.code)
mLastTapTime = eventTime
}
}
@@ -541,16 +587,17 @@ class MainKeyboard @JvmOverloads constructor(
if (mCurrentKeyIndex != NOT_A_KEY && keys.size > mCurrentKeyIndex) {
val newKey = keys[mCurrentKeyIndex]
+ val keyCode = newKey.code
- val code = newKey.code
- if (code == KEYCODE_SHIFT || code == KEYCODE_MODE_CHANGE || code == KEYCODE_DELETE || code == KEYCODE_ENTER || code == KEYCODE_SPACE) {
+ if (keyCode == KEYCODE_SHIFT || keyCode == KEYCODE_MODE_CHANGE || keyCode == KEYCODE_DELETE || keyCode == KEYCODE_ENTER || keyCode == KEYCODE_SPACE ||
+ keyCode == KEYCODE_TAB || keyCode == KEYCODE_ARROW_LEFT || keyCode == KEYCODE_ARROW_RIGHT || keyCode == KEYCODE_ARROW_UP || keyCode == KEYCODE_ARROW_DOWN) {
newKey.pressed = true
}
invalidateKey(mCurrentKeyIndex)
sendAccessibilityEventForUnicodeCharacter(
AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED,
- code
+ keyCode
)
}
}
@@ -664,13 +711,20 @@ class MainKeyboard @JvmOverloads constructor(
private fun sendAccessibilityEventForUnicodeCharacter(eventType: Int, code: Int) {
if (mAccessibilityManager.isEnabled) {
- val event = AccessibilityEvent.obtain(eventType)
+ val event = AccessibilityEvent().apply {
+ this.eventType = eventType
+ }
onInitializeAccessibilityEvent(event)
val text: String = when (code) {
KEYCODE_DELETE -> context.getString(R.string.keycode_delete)
KEYCODE_ENTER -> context.getString(R.string.keycode_enter)
KEYCODE_MODE_CHANGE -> context.getString(R.string.keycode_mode_change)
KEYCODE_SHIFT -> context.getString(R.string.keycode_shift)
+ KEYCODE_TAB -> "Tab"
+ KEYCODE_ARROW_LEFT -> "Left Arrow"
+ KEYCODE_ARROW_RIGHT -> "Right Arrow"
+ KEYCODE_ARROW_UP -> "Up Arrow"
+ KEYCODE_ARROW_DOWN -> "Down Arrow"
else -> code.toChar().toString()
}
event.text.add(text)
@@ -704,10 +758,7 @@ class MainKeyboard @JvmOverloads constructor(
key.x + key.width, key.y + key.height
)
onBufferDraw()
- invalidate(
- key.x, key.y,
- key.x + key.width, key.y + key.height
- )
+ invalidate()
}
private fun openPopupIfRequired(me: MotionEvent): Boolean {
@@ -756,28 +807,28 @@ class MainKeyboard @JvmOverloads constructor(
mMiniKeyboard!!.mOnKeyboardActionListener = object : OnKeyboardActionListener {
override fun onKey(code: Int) {
- mOnKeyboardActionListener!!.onKey(code)
+ mOnKeyboardActionListener?.onKey(code)
dismissPopupKeyboard()
}
override fun onPress(primaryCode: Int) {
- mOnKeyboardActionListener!!.onPress(primaryCode)
+ mOnKeyboardActionListener?.onPress(primaryCode)
}
override fun onActionUp() {
- mOnKeyboardActionListener!!.onActionUp()
+ mOnKeyboardActionListener?.onActionUp()
}
override fun moveCursorLeft() {
- mOnKeyboardActionListener!!.moveCursorLeft()
+ mOnKeyboardActionListener?.moveCursorLeft()
}
override fun moveCursorRight() {
- mOnKeyboardActionListener!!.moveCursorRight()
+ mOnKeyboardActionListener?.moveCursorRight()
}
override fun onText(text: String) {
- mOnKeyboardActionListener!!.onText(text)
+ mOnKeyboardActionListener?.onText(text)
}
}
@@ -918,7 +969,7 @@ class MainKeyboard @JvmOverloads constructor(
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
mMiniKeyboard?.mKeys?.firstOrNull { it.focused }?.apply {
- mOnKeyboardActionListener!!.onKey(code)
+ mOnKeyboardActionListener?.onKey(code)
}
mMiniKeyboardSelectedKeyIndex = -1
dismissPopupKeyboard()
@@ -967,7 +1018,7 @@ class MainKeyboard @JvmOverloads constructor(
val secondKeyCode = mKeys.getOrNull(secondKeyIndex)?.code
if (secondKeyCode != null) {
- mOnKeyboardActionListener!!.onPress(secondKeyCode)
+ mOnKeyboardActionListener?.onPress(secondKeyCode)
}
showPreview(NOT_A_KEY)
@@ -992,7 +1043,7 @@ class MainKeyboard @JvmOverloads constructor(
0
}
- mOnKeyboardActionListener!!.onPress(onPressKey)
+ mOnKeyboardActionListener?.onPress(onPressKey)
var wasHandled = false
if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) {
@@ -1100,15 +1151,14 @@ class MainKeyboard @JvmOverloads constructor(
// 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)
- }
-
- if (mKeys.getOrNull(mCurrentKey)?.code == KEYCODE_SPACE && !mIsLongPressingSpace) {
+ } 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)
mRepeatKeyIndex = NOT_A_KEY
- mOnKeyboardActionListener!!.onActionUp()
+ mOnKeyboardActionListener?.onActionUp()
mIsLongPressingSpace = false
}
diff --git a/frogo-keyboard/src/main/res/drawable/ic_keyboard_arrow_down.xml b/frogo-keyboard/src/main/res/drawable/ic_keyboard_arrow_down.xml
new file mode 100644
index 0000000..50613c2
--- /dev/null
+++ b/frogo-keyboard/src/main/res/drawable/ic_keyboard_arrow_down.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/frogo-keyboard/src/main/res/drawable/ic_keyboard_arrow_up.xml b/frogo-keyboard/src/main/res/drawable/ic_keyboard_arrow_up.xml
new file mode 100644
index 0000000..bd1add9
--- /dev/null
+++ b/frogo-keyboard/src/main/res/drawable/ic_keyboard_arrow_up.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/frogo-keyboard/src/main/res/drawable/ic_keyboard_tab.xml b/frogo-keyboard/src/main/res/drawable/ic_keyboard_tab.xml
new file mode 100644
index 0000000..a79155f
--- /dev/null
+++ b/frogo-keyboard/src/main/res/drawable/ic_keyboard_tab.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/frogo-keyboard/src/main/res/raw/mechanical_cherry_blue.wav b/frogo-keyboard/src/main/res/raw/mechanical_cherry_blue.wav
new file mode 100644
index 0000000..11354cf
Binary files /dev/null and b/frogo-keyboard/src/main/res/raw/mechanical_cherry_blue.wav differ
diff --git a/frogo-keyboard/src/main/res/raw/mechanical_cherry_brown.wav b/frogo-keyboard/src/main/res/raw/mechanical_cherry_brown.wav
new file mode 100644
index 0000000..2f8a41d
Binary files /dev/null and b/frogo-keyboard/src/main/res/raw/mechanical_cherry_brown.wav differ
diff --git a/frogo-keyboard/src/main/res/raw/mechanical_cherry_red.wav b/frogo-keyboard/src/main/res/raw/mechanical_cherry_red.wav
new file mode 100644
index 0000000..ba5b601
Binary files /dev/null and b/frogo-keyboard/src/main/res/raw/mechanical_cherry_red.wav differ
diff --git a/frogo-keyboard/src/main/res/raw/mechanical_typewriter.wav b/frogo-keyboard/src/main/res/raw/mechanical_typewriter.wav
new file mode 100644
index 0000000..c6a3d63
Binary files /dev/null and b/frogo-keyboard/src/main/res/raw/mechanical_typewriter.wav differ
diff --git a/frogo-keyboard/src/main/res/xml/keys_letters_programmer.xml b/frogo-keyboard/src/main/res/xml/keys_letters_programmer.xml
new file mode 100644
index 0000000..e7d4024
--- /dev/null
+++ b/frogo-keyboard/src/main/res/xml/keys_letters_programmer.xml
@@ -0,0 +1,253 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gradle.properties b/gradle.properties
index b5b0a35..5655886 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -6,19 +6,23 @@
# 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=-Xmx4608m
-# When configured, Gradle will run in incubating parallel mode.
-# This option should only be used with decoupled projects. More details, visit
-# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
-org.gradle.daemon=false
+org.gradle.jvmargs=-Xmx1280m -XX:+UseParallelGC -XX:MaxMetaspaceSize=320m -Dfile.encoding=UTF-8
+# Gradle Performance Flags
+org.gradle.daemon=true
org.gradle.parallel=false
+org.gradle.workers.max=1
+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
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
-# Automatically convert third-party libraries to use AndroidX
-android.enableJetifier=true
+# Automatically convert third-party libraries to use AndroidX (disabled because all libs are modern AndroidX)
+android.enableJetifier=false
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
-android.nonTransitiveRClass=false
-android.nonFinalResIds=false
+android.nonTransitiveRClass=true
+ksp.incremental=false
+kotlin.incremental=false
+
+# 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 224b4be..1369c39 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,24 +1,41 @@
[versions]
-agp = "8.13.0"
-kotlin = "2.2.21"
-ksp = "2.2.21-2.0.4"
-liveCycle = "2.9.4"
+agp = "9.4.1"
+kotlin = "2.4.20"
+ksp = "2.3.10"
+liveCycle = "2.11.0"
-roomKtx = "2.8.3"
-work = "2.11.0"
-glideCompiler = "5.0.5"
+roomKtx = "2.8.5"
+work = "2.11.2"
+glideCompiler = "5.0.9"
-hilt = "2.57.2"
+hilt = "2.60.1"
emoji2 = "1.6.0"
-frogoAndroid = "2.3.6"
-frogoconsumeapi = "2.6.1"
+frogoAndroid = "3.0.10"
+frogoconsumeapi = "3.0.0"
+
+composeBom = "2026.09.00"
+activityCompose = "1.13.0"
+junit = "4.13.2"
[libraries]
+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-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" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "roomKtx" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
+androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "liveCycle" }
+
google-hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
google-hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
@@ -31,7 +48,7 @@ frogo-consume-api = { group = "com.github.frogobox", name = "frogo-consume-api",
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp"}
-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin"}
+jetbrains-kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"}
-hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
+hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 46fd8dc..f8db6e2 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Sun Nov 03 18:01:05 WIB 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists