Merge branch 'new-settings-menu' into 'master'
New settings menu See merge request alex/latinime!2
@ -163,6 +163,8 @@ dependencies {
|
||||
implementation 'ch.acra:acra-http:5.11.1'
|
||||
implementation 'ch.acra:acra-dialog:5.11.1'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
||||
|
||||
implementation project(":voiceinput-shared")
|
||||
|
||||
debugImplementation 'androidx.compose.ui:ui-tooling'
|
||||
|
@ -52,7 +52,8 @@
|
||||
android:protectionLevel="signature"/>
|
||||
|
||||
<application android:label="@string/english_ime_name"
|
||||
android:icon="@drawable/ic_launcher_keyboard"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:allowBackup="true"
|
||||
android:defaultToDeviceProtectedStorage="true"
|
||||
@ -87,12 +88,13 @@
|
||||
</service>
|
||||
|
||||
<!-- Activities -->
|
||||
<activity android:name=".setup.SetupActivity"
|
||||
android:theme="@style/platformActivityTheme"
|
||||
<activity android:name=".uix.settings.SettingsActivity"
|
||||
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"
|
||||
android:label="@string/english_ime_name"
|
||||
android:icon="@drawable/ic_launcher_keyboard"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:launchMode="singleTask"
|
||||
android:noHistory="true"
|
||||
android:noHistory="false"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
@ -100,31 +102,20 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".uix.voiceinput.downloader.DownloadActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/model_downloader"
|
||||
android:clearTaskOnLaunch="true"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".permissions.PermissionsActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:exported="false"
|
||||
android:taskAffinity="">
|
||||
</activity>
|
||||
|
||||
<activity android:name=".setup.SetupWizardActivity"
|
||||
android:theme="@style/platformActivityTheme"
|
||||
android:label="@string/english_ime_name"
|
||||
android:clearTaskOnLaunch="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".settings.SettingsActivity"
|
||||
android:theme="@style/platformSettingsTheme"
|
||||
android:label="@string/english_ime_settings"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".spellcheck.SpellCheckerSettingsActivity"
|
||||
android:theme="@style/platformSettingsTheme"
|
||||
android:label="@string/android_spell_checker_settings"
|
||||
|
BIN
java/ic_launcher-playstore.png
Normal file
After Width: | Height: | Size: 16 KiB |
11
java/res/drawable/futo_logo.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="92dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="92"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M91.636,12C91.636,18.627 86.267,24 79.644,24C73.02,24 67.651,18.627 67.651,12C67.651,5.373 73.02,0 79.644,0C86.267,0 91.636,5.373 91.636,12ZM76.15,14.422C74.92,13.191 74.305,12.575 74.305,11.811C74.305,11.046 74.92,10.431 76.15,9.2L77.153,8.197C78.383,6.966 78.998,6.351 79.762,6.351C80.526,6.351 81.141,6.966 82.371,8.197L83.374,9.2C84.604,10.431 85.219,11.046 85.219,11.811C85.219,12.575 84.604,13.191 83.374,14.422L82.371,15.425C81.141,16.655 80.526,17.271 79.762,17.271C78.998,17.271 78.383,16.655 77.153,15.425L76.15,14.422ZM16.913,7.077C17.252,7.077 17.528,6.801 17.528,6.462V1.846C17.528,1.506 17.252,1.231 16.913,1.231H0.615C0.275,1.231 0,1.506 0,1.846V22.154C0,22.494 0.275,22.769 0.615,22.769H6.15C6.49,22.769 6.765,22.494 6.765,22.154V16.492C6.765,16.152 7.04,15.877 7.38,15.877H14.822C15.161,15.877 15.437,15.601 15.437,15.262V10.646C15.437,10.306 15.161,10.031 14.822,10.031H7.38C7.04,10.031 6.765,9.755 6.765,9.415V7.692C6.765,7.352 7.04,7.077 7.38,7.077H16.913ZM31.209,23.139H31.302C37.882,23.139 41.91,19.631 41.91,12.954V1.846C41.91,1.506 41.635,1.231 41.295,1.231H35.76C35.421,1.231 35.145,1.506 35.145,1.846V12.339C35.145,14.615 34.161,16.615 31.302,16.615H31.209C28.38,16.615 27.365,14.615 27.365,12.339V1.846C27.365,1.506 27.09,1.231 26.75,1.231H21.215C20.876,1.231 20.6,1.506 20.6,1.846V12.954C20.6,19.631 24.629,23.139 31.209,23.139ZM44.985,1.846C44.985,1.506 45.26,1.231 45.599,1.231H65.464C65.804,1.231 66.079,1.506 66.079,1.846V6.554C66.079,6.894 65.804,7.169 65.464,7.169H59.529C59.19,7.169 58.915,7.445 58.915,7.785V22.154C58.915,22.494 58.639,22.769 58.299,22.769H52.764C52.425,22.769 52.149,22.494 52.149,22.154V7.785C52.149,7.445 51.874,7.169 51.534,7.169H45.599C45.26,7.169 44.985,6.894 44.985,6.554V1.846Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
52
java/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,52 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="128"
|
||||
android:viewportHeight="128">
|
||||
<group android:scaleX="0.63"
|
||||
android:scaleY="0.63"
|
||||
android:translateX="23.68"
|
||||
android:translateY="23.68">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h128v128h-128z"/>
|
||||
<path
|
||||
android:pathData="M114.5,49.5C114.5,56.96 108.58,63 101.28,63C93.98,63 88.06,56.96 88.06,49.5C88.06,42.04 93.98,36 101.28,36C108.58,36 114.5,42.04 114.5,49.5ZZM97.43,52.22C96.08,50.84 95.4,50.15 95.4,49.29C95.4,48.43 96.08,47.73 97.43,46.35L98.54,45.22C99.89,43.84 100.57,43.14 101.41,43.14C102.25,43.14 102.93,43.84 104.29,45.22L105.39,46.35C106.75,47.73 107.43,48.43 107.43,49.29C107.43,50.15 106.75,50.84 105.39,52.22L104.29,53.35C102.93,54.74 102.25,55.43 101.41,55.43C100.57,55.43 99.89,54.74 98.54,53.35L97.43,52.22ZZM32.14,43.96C32.51,43.96 32.82,43.65 32.82,43.27L32.82,38.08C32.82,37.69 32.51,37.38 32.14,37.38L14.18,37.38C13.8,37.38 13.5,37.69 13.5,38.08L13.5,60.92C13.5,61.31 13.8,61.62 14.18,61.62L20.28,61.62C20.65,61.62 20.96,61.31 20.96,60.92L20.96,54.55C20.96,54.17 21.26,53.86 21.63,53.86L29.84,53.86C30.21,53.86 30.51,53.55 30.51,53.17L30.51,47.98C30.51,47.6 30.21,47.28 29.84,47.28L21.63,47.28C21.26,47.28 20.96,46.97 20.96,46.59L20.96,44.65C20.96,44.27 21.26,43.96 21.63,43.96L32.14,43.96ZZM47.9,62.03L48,62.03C55.25,62.03 59.69,58.08 59.69,50.57L59.69,38.08C59.69,37.69 59.39,37.38 59.01,37.38L52.91,37.38C52.54,37.38 52.24,37.69 52.24,38.08L52.24,49.88C52.24,52.44 51.15,54.69 48,54.69L47.9,54.69C44.78,54.69 43.66,52.44 43.66,49.88L43.66,38.08C43.66,37.69 43.36,37.38 42.98,37.38L36.88,37.38C36.51,37.38 36.21,37.69 36.21,38.08L36.21,50.57C36.21,58.08 40.65,62.03 47.9,62.03ZZM63.08,38.08C63.08,37.69 63.38,37.38 63.76,37.38L85.65,37.38C86.03,37.38 86.33,37.69 86.33,38.08L86.33,43.37C86.33,43.76 86.03,44.06 85.65,44.06L79.11,44.06C78.74,44.06 78.43,44.38 78.43,44.76L78.43,60.92C78.43,61.31 78.13,61.62 77.76,61.62L71.66,61.62C71.28,61.62 70.98,61.31 70.98,60.92L70.98,44.76C70.98,44.38 70.68,44.06 70.3,44.06L63.76,44.06C63.38,44.06 63.08,43.76 63.08,43.37L63.08,38.08ZZ"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M35,72L40.54,72A2,2 0,0 1,42.54 74L42.54,79.6A2,2 0,0 1,40.54 81.6L35,81.6A2,2 0,0 1,33 79.6L33,74A2,2 0,0 1,35 72z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M35,85.2L40.54,85.2A2,2 0,0 1,42.54 87.2L42.54,92.8A2,2 0,0 1,40.54 94.8L35,94.8A2,2 0,0 1,33 92.8L33,87.2A2,2 0,0 1,35 85.2z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M48.12,72L53.65,72A2,2 0,0 1,55.65 74L55.65,79.6A2,2 0,0 1,53.65 81.6L48.12,81.6A2,2 0,0 1,46.12 79.6L46.12,74A2,2 0,0 1,48.12 72z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M48.12,85.2L53.65,85.2A2,2 0,0 1,55.65 87.2L55.65,92.8A2,2 0,0 1,53.65 94.8L48.12,94.8A2,2 0,0 1,46.12 92.8L46.12,87.2A2,2 0,0 1,48.12 85.2z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M61.23,72L66.77,72A2,2 0,0 1,68.77 74L68.77,79.6A2,2 0,0 1,66.77 81.6L61.23,81.6A2,2 0,0 1,59.23 79.6L59.23,74A2,2 0,0 1,61.23 72z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M61.23,85.2L66.77,85.2A2,2 0,0 1,68.77 87.2L68.77,92.8A2,2 0,0 1,66.77 94.8L61.23,94.8A2,2 0,0 1,59.23 92.8L59.23,87.2A2,2 0,0 1,61.23 85.2z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M45.73,98.4L82.27,98.4A2,2 0,0 1,84.27 100.4L84.27,106A2,2 0,0 1,82.27 108L45.73,108A2,2 0,0 1,43.73 106L43.73,100.4A2,2 0,0 1,45.73 98.4z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M74.35,72L79.88,72A2,2 0,0 1,81.88 74L81.88,79.6A2,2 0,0 1,79.88 81.6L74.35,81.6A2,2 0,0 1,72.35 79.6L72.35,74A2,2 0,0 1,74.35 72z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M74.35,85.2L79.88,85.2A2,2 0,0 1,81.88 87.2L81.88,92.8A2,2 0,0 1,79.88 94.8L74.35,94.8A2,2 0,0 1,72.35 92.8L72.35,87.2A2,2 0,0 1,74.35 85.2z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M87.46,72L93,72A2,2 0,0 1,95 74L95,79.6A2,2 0,0 1,93 81.6L87.46,81.6A2,2 0,0 1,85.46 79.6L85.46,74A2,2 0,0 1,87.46 72z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M87.46,85.2L93,85.2A2,2 0,0 1,95 87.2L95,92.8A2,2 0,0 1,93 94.8L87.46,94.8A2,2 0,0 1,85.46 92.8L85.46,87.2A2,2 0,0 1,87.46 85.2z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
5
java/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
5
java/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
BIN
java/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
java/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
java/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
java/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
java/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
java/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
java/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
java/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
java/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
java/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 12 KiB |
4
java/res/values/ic_launcher_background.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#1E293B</color>
|
||||
</resources>
|
@ -5,9 +5,20 @@
|
||||
|
||||
<string name="amoled_dark_theme_name">AMOLED Dark Purple</string>
|
||||
<string name="classic_material_dark_theme_name">AOSP Material Dark</string>
|
||||
<string name="voice_input_theme_name">Voice Input Theme</string>
|
||||
<string name="classic_material_light_theme_name">AOSP Material Light</string>
|
||||
<string name="voice_input_theme_name">FUTO VI Theme</string>
|
||||
|
||||
<string name="dynamic_system_theme_name">Dynamic System</string>
|
||||
<string name="dynamic_light_theme_name">Dynamic Light</string>
|
||||
<string name="dynamic_dark_theme_name">Dynamic Dark</string>
|
||||
|
||||
<string name="download_required">Voice Input Model Downloader</string>
|
||||
<string name="download_required_body">Download of some model files is necessary for voice input functionality. This may incur data fees if you are using Mobile Data instead of Wi-Fi.</string>
|
||||
<string name="continue_">Continue</string>
|
||||
|
||||
<string name="download_progress">Voice Input Model Download Progress</string>
|
||||
<string name="download_failed">Download of one or more resources has failed. Please make sure you\'re connected to a network, the app has network permission, or try again later.</string>
|
||||
<string name="download_in_progress">Downloading models…</string>
|
||||
|
||||
<string name="model_downloader">Model Downloader</string>
|
||||
</resources>
|
@ -113,7 +113,7 @@
|
||||
<!-- If IME doesn't have an applicable subtype, the first subtype will be used as a default
|
||||
subtype.-->
|
||||
<input-method xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:settingsActivity="org.futo.inputmethod.latin.settings.SettingsActivity"
|
||||
android:settingsActivity="org.futo.inputmethod.latin.uix.settings.SettingsActivity"
|
||||
android:isDefault="@bool/im_is_default"
|
||||
android:supportsSwitchingToNextInputMethod="true"
|
||||
android:supportsInlineSuggestions="true">
|
||||
|
@ -144,6 +144,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
|
||||
|
||||
private var lastEditorInfo: EditorInfo? = null
|
||||
|
||||
// TODO: Calling this repeatedly as the theme changes tends to slow everything to a crawl
|
||||
private fun recreateKeyboard() {
|
||||
latinIMELegacy.updateTheme()
|
||||
latinIMELegacy.mKeyboardSwitcher.mState.onLoadKeyboard(latinIMELegacy.currentAutoCapsState, latinIMELegacy.currentRecapitalizeState);
|
||||
@ -178,6 +179,13 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
|
||||
|
||||
if(currColors.differsFrom(nextColors)) {
|
||||
updateDrawableProvider(nextColors)
|
||||
recreateKeyboard()
|
||||
}
|
||||
}
|
||||
|
||||
deferGetSetting(THEME_KEY) { key ->
|
||||
if(key != activeThemeOption?.key) {
|
||||
ThemeOptions[key]?.let { updateTheme(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -185,12 +193,12 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
colorSchemeLoaderJob = deferGetSetting(THEME_KEY, DynamicSystemTheme.key) {
|
||||
var themeKey = it
|
||||
var themeOption = ThemeOptions[themeKey]
|
||||
if (themeOption == null || !themeOption.available(this@LatinIME)) {
|
||||
themeKey = VoiceInputTheme.key
|
||||
themeOption = ThemeOptions[themeKey]!!
|
||||
colorSchemeLoaderJob = deferGetSetting(THEME_KEY) {
|
||||
val themeOptionFromSettings = ThemeOptions[it]
|
||||
val themeOption = when {
|
||||
themeOptionFromSettings == null -> VoiceInputTheme
|
||||
!themeOptionFromSettings.available(this@LatinIME) -> VoiceInputTheme
|
||||
else -> themeOptionFromSettings
|
||||
}
|
||||
|
||||
activeThemeOption = themeOption
|
||||
@ -304,13 +312,18 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
|
||||
}
|
||||
|
||||
private fun returnBackToMainKeyboardViewFromAction() {
|
||||
assert(currWindowActionWindow != null)
|
||||
if(currWindowActionWindow == null) return
|
||||
|
||||
currWindowActionWindow!!.close()
|
||||
|
||||
currWindowAction = null
|
||||
currWindowActionWindow = null
|
||||
|
||||
if(hasThemeChanged) {
|
||||
hasThemeChanged = false
|
||||
recreateKeyboard()
|
||||
}
|
||||
|
||||
setContent()
|
||||
}
|
||||
|
||||
@ -418,11 +431,15 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
|
||||
override fun onFinishInputView(finishingInput: Boolean) {
|
||||
super.onFinishInputView(finishingInput)
|
||||
latinIMELegacy.onFinishInputView(finishingInput)
|
||||
|
||||
closeActionWindow()
|
||||
}
|
||||
|
||||
override fun onFinishInput() {
|
||||
super.onFinishInput()
|
||||
latinIMELegacy.onFinishInput()
|
||||
|
||||
closeActionWindow()
|
||||
}
|
||||
|
||||
override fun onCurrentInputMethodSubtypeChanged(newSubtype: InputMethodSubtype?) {
|
||||
@ -440,6 +457,8 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
|
||||
override fun onWindowHidden() {
|
||||
super.onWindowHidden()
|
||||
latinIMELegacy.onWindowHidden()
|
||||
|
||||
closeActionWindow()
|
||||
}
|
||||
|
||||
override fun onUpdateSelection(
|
||||
@ -636,6 +655,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
|
||||
}
|
||||
|
||||
override fun closeActionWindow() {
|
||||
if(currWindowActionWindow == null) return
|
||||
returnBackToMainKeyboardViewFromAction()
|
||||
}
|
||||
|
||||
@ -648,14 +668,20 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
|
||||
);
|
||||
}
|
||||
|
||||
private var hasThemeChanged: Boolean = false
|
||||
override fun updateTheme(newTheme: ThemeOption) {
|
||||
assert(newTheme.available(this))
|
||||
activeThemeOption = newTheme
|
||||
updateDrawableProvider(newTheme.obtainColors(this))
|
||||
|
||||
deferSetSetting(THEME_KEY, newTheme.key)
|
||||
if (activeThemeOption != newTheme) {
|
||||
activeThemeOption = newTheme
|
||||
updateDrawableProvider(newTheme.obtainColors(this))
|
||||
deferSetSetting(THEME_KEY, newTheme.key)
|
||||
|
||||
recreateKeyboard()
|
||||
hasThemeChanged = true
|
||||
if(!isActionWindowOpen()) {
|
||||
recreateKeyboard()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
|
@ -85,12 +85,12 @@ import org.futo.inputmethod.latin.inputlogic.InputLogic;
|
||||
import org.futo.inputmethod.latin.permissions.PermissionsManager;
|
||||
import org.futo.inputmethod.latin.personalization.PersonalizationHelper;
|
||||
import org.futo.inputmethod.latin.settings.Settings;
|
||||
import org.futo.inputmethod.latin.settings.SettingsActivity;
|
||||
import org.futo.inputmethod.latin.settings.SettingsValues;
|
||||
import org.futo.inputmethod.latin.suggestions.SuggestionStripView;
|
||||
import org.futo.inputmethod.latin.suggestions.SuggestionStripViewAccessor;
|
||||
import org.futo.inputmethod.latin.touchinputconsumer.GestureConsumer;
|
||||
import org.futo.inputmethod.latin.uix.DynamicThemeProviderOwner;
|
||||
import org.futo.inputmethod.latin.uix.settings.SettingsActivity;
|
||||
import org.futo.inputmethod.latin.utils.ApplicationUtils;
|
||||
import org.futo.inputmethod.latin.utils.DialogUtils;
|
||||
import org.futo.inputmethod.latin.utils.ImportantNoticeUtils;
|
||||
@ -1803,8 +1803,8 @@ public class LatinIMELegacy implements KeyboardActionListener,
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
|
||||
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
intent.putExtra(SettingsActivity.EXTRA_SHOW_HOME_AS_UP, false);
|
||||
intent.putExtra(SettingsActivity.EXTRA_ENTRY_KEY, extraEntryValue);
|
||||
//intent.putExtra(SettingsActivity.EXTRA_SHOW_HOME_AS_UP, false);
|
||||
//intent.putExtra(SettingsActivity.EXTRA_ENTRY_KEY, extraEntryValue);
|
||||
startActivityOnTheSameDisplay(intent);
|
||||
}
|
||||
|
||||
@ -1832,7 +1832,7 @@ public class LatinIMELegacy implements KeyboardActionListener,
|
||||
startActivityOnTheSameDisplay(intent);
|
||||
break;
|
||||
case 1:
|
||||
launchSettings(SettingsActivity.EXTRA_ENTRY_VALUE_LONG_PRESS_COMMA);
|
||||
launchSettings("");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -143,6 +143,7 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver {
|
||||
}
|
||||
|
||||
public static void toggleAppIcon(final Context context) {
|
||||
/*
|
||||
final int appInfoFlags = context.getApplicationInfo().flags;
|
||||
final boolean isSystemApp = (appInfoFlags & ApplicationInfo.FLAG_SYSTEM) > 0;
|
||||
if (Log.isLoggable(TAG, Log.INFO)) {
|
||||
@ -155,5 +156,6 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver {
|
||||
? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
: PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
||||
PackageManager.DONT_KILL_APP);
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
@ -1,88 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.futo.inputmethod.latin.settings;
|
||||
|
||||
import org.futo.inputmethod.latin.permissions.PermissionsManager;
|
||||
import org.futo.inputmethod.latin.utils.FragmentUtils;
|
||||
import org.futo.inputmethod.latin.utils.StatsUtils;
|
||||
import org.futo.inputmethod.latin.utils.StatsUtilsManager;
|
||||
|
||||
import android.app.ActionBar;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import android.view.MenuItem;
|
||||
|
||||
public final class SettingsActivity extends PreferenceActivity
|
||||
implements ActivityCompat.OnRequestPermissionsResultCallback {
|
||||
private static final String DEFAULT_FRAGMENT = SettingsFragment.class.getName();
|
||||
|
||||
public static final String EXTRA_SHOW_HOME_AS_UP = "show_home_as_up";
|
||||
public static final String EXTRA_ENTRY_KEY = "entry";
|
||||
public static final String EXTRA_ENTRY_VALUE_LONG_PRESS_COMMA = "long_press_comma";
|
||||
public static final String EXTRA_ENTRY_VALUE_APP_ICON = "app_icon";
|
||||
public static final String EXTRA_ENTRY_VALUE_NOTICE_DIALOG = "important_notice";
|
||||
public static final String EXTRA_ENTRY_VALUE_SYSTEM_SETTINGS = "system_settings";
|
||||
|
||||
private boolean mShowHomeAsUp;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
final ActionBar actionBar = getActionBar();
|
||||
final Intent intent = getIntent();
|
||||
if (actionBar != null) {
|
||||
mShowHomeAsUp = intent.getBooleanExtra(EXTRA_SHOW_HOME_AS_UP, true);
|
||||
actionBar.setDisplayHomeAsUpEnabled(mShowHomeAsUp);
|
||||
actionBar.setHomeButtonEnabled(mShowHomeAsUp);
|
||||
}
|
||||
StatsUtils.onSettingsActivity(
|
||||
intent.hasExtra(EXTRA_ENTRY_KEY) ? intent.getStringExtra(EXTRA_ENTRY_KEY)
|
||||
: EXTRA_ENTRY_VALUE_SYSTEM_SETTINGS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
if (mShowHomeAsUp && item.getItemId() == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Intent getIntent() {
|
||||
final Intent intent = super.getIntent();
|
||||
final String fragment = intent.getStringExtra(EXTRA_SHOW_FRAGMENT);
|
||||
if (fragment == null) {
|
||||
intent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT);
|
||||
}
|
||||
intent.putExtra(EXTRA_NO_HEADERS, true);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidFragment(final String fragmentName) {
|
||||
return FragmentUtils.isValidFragment(fragmentName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
PermissionsManager.get(this).onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@ import android.view.MenuItem;
|
||||
|
||||
import org.futo.inputmethod.latin.R;
|
||||
import org.futo.inputmethod.latin.define.ProductionFlags;
|
||||
import org.futo.inputmethod.latin.uix.settings.SettingsActivity;
|
||||
import org.futo.inputmethod.latin.utils.ApplicationUtils;
|
||||
import org.futo.inputmethod.latin.utils.FeedbackUtils;
|
||||
import org.futo.inputmethodcommon.InputMethodSettingsFragment;
|
||||
|
@ -36,7 +36,7 @@ import android.widget.VideoView;
|
||||
import org.futo.inputmethod.compat.TextViewCompatUtils;
|
||||
import org.futo.inputmethod.compat.ViewCompatUtils;
|
||||
import org.futo.inputmethod.latin.R;
|
||||
import org.futo.inputmethod.latin.settings.SettingsActivity;
|
||||
import org.futo.inputmethod.latin.uix.settings.SettingsActivity;
|
||||
import org.futo.inputmethod.latin.utils.LeakGuardHandlerWrapper;
|
||||
import org.futo.inputmethod.latin.utils.UncachedInputMethodManagerUtils;
|
||||
|
||||
@ -265,8 +265,8 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL
|
||||
intent.setClass(this, SettingsActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
|
||||
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
intent.putExtra(SettingsActivity.EXTRA_ENTRY_KEY,
|
||||
SettingsActivity.EXTRA_ENTRY_VALUE_APP_ICON);
|
||||
//intent.putExtra(SettingsActivity.EXTRA_ENTRY_KEY,
|
||||
// SettingsActivity.EXTRA_ENTRY_VALUE_APP_ICON);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
|
@ -1,5 +0,0 @@
|
||||
@file:Suppress("LocalVariableName")
|
||||
|
||||
package org.futo.inputmethod.latin.uix
|
||||
|
||||
|
@ -21,8 +21,6 @@ import org.futo.inputmethod.latin.R
|
||||
import org.futo.inputmethod.latin.uix.theme.DarkColorScheme
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
// TODO: Expand the number of drawables this provides so it covers the full theme, and
|
||||
// build some system to dynamically change these colors
|
||||
class BasicThemeProvider(val context: Context, val overrideColorScheme: ColorScheme? = null) :
|
||||
DynamicThemeProvider {
|
||||
override val primaryKeyboardColor: Int
|
||||
|
@ -6,7 +6,6 @@ import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -18,6 +17,7 @@ import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.futo.inputmethod.latin.uix.theme.presets.DynamicSystemTheme
|
||||
|
||||
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
||||
|
||||
@ -28,6 +28,10 @@ suspend fun <T> Context.getSetting(key: Preferences.Key<T>, default: T): T {
|
||||
return valueFlow.first()
|
||||
}
|
||||
|
||||
fun <T> Context.getSettingFlow(key: Preferences.Key<T>, default: T): Flow<T> {
|
||||
return dataStore.data.map { preferences -> preferences[key] ?: default }.take(1)
|
||||
}
|
||||
|
||||
suspend fun <T> Context.setSetting(key: Preferences.Key<T>, value: T) {
|
||||
this.dataStore.edit { preferences ->
|
||||
preferences[key] = value
|
||||
@ -55,7 +59,10 @@ fun <T> LifecycleOwner.deferGetSetting(key: Preferences.Key<T>, default: T, onOb
|
||||
return lifecycleScope.launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
val value = context.getSetting(key, default)
|
||||
onObtained(value)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
onObtained(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -75,16 +82,27 @@ data class SettingsKey<T>(
|
||||
)
|
||||
|
||||
suspend fun <T> Context.getSetting(key: SettingsKey<T>): T {
|
||||
val valueFlow: Flow<T> =
|
||||
this.dataStore.data.map { preferences -> preferences[key.key] ?: key.default }.take(1)
|
||||
return getSetting(key.key, key.default)
|
||||
}
|
||||
|
||||
return valueFlow.first()
|
||||
fun <T> Context.getSettingFlow(key: SettingsKey<T>): Flow<T> {
|
||||
return getSettingFlow(key.key, key.default)
|
||||
}
|
||||
|
||||
suspend fun <T> Context.setSetting(key: SettingsKey<T>, value: T) {
|
||||
this.dataStore.edit { preferences ->
|
||||
preferences[key.key] = value
|
||||
}
|
||||
return setSetting(key.key, value)
|
||||
}
|
||||
|
||||
val THEME_KEY = stringPreferencesKey("activeThemeOption")
|
||||
fun <T> LifecycleOwner.deferGetSetting(key: SettingsKey<T>, onObtained: (T) -> Unit): Job {
|
||||
return deferGetSetting(key.key, key.default, onObtained)
|
||||
}
|
||||
|
||||
fun <T> LifecycleOwner.deferSetSetting(key: SettingsKey<T>, value: T): Job {
|
||||
return deferSetSetting(key.key, value)
|
||||
}
|
||||
|
||||
|
||||
val THEME_KEY = SettingsKey(
|
||||
key = stringPreferencesKey("activeThemeOption"),
|
||||
default = DynamicSystemTheme.key
|
||||
)
|
@ -16,6 +16,7 @@ import org.futo.inputmethod.latin.uix.ActionWindow
|
||||
import org.futo.inputmethod.latin.uix.KeyboardManagerForAction
|
||||
import org.futo.inputmethod.latin.uix.theme.ThemeOptionKeys
|
||||
import org.futo.inputmethod.latin.uix.theme.ThemeOptions
|
||||
import org.futo.inputmethod.latin.uix.theme.selector.ThemePicker
|
||||
|
||||
val ThemeAction = Action(
|
||||
icon = R.drawable.eye,
|
||||
@ -31,6 +32,9 @@ val ThemeAction = Action(
|
||||
@Composable
|
||||
override fun WindowContents() {
|
||||
val context = LocalContext.current
|
||||
|
||||
ThemePicker { manager.updateTheme(it) }
|
||||
/*
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(8.dp, 0.dp)
|
||||
@ -51,6 +55,8 @@ val ThemeAction = Action(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
|
@ -1,15 +1,20 @@
|
||||
package org.futo.inputmethod.latin.uix.actions
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
@ -32,6 +37,7 @@ import org.futo.inputmethod.latin.uix.MULTILINGUAL_MODEL_INDEX
|
||||
import org.futo.inputmethod.latin.uix.PersistentActionState
|
||||
import org.futo.inputmethod.latin.uix.VERBOSE_PROGRESS
|
||||
import org.futo.inputmethod.latin.uix.getSetting
|
||||
import org.futo.inputmethod.latin.uix.voiceinput.downloader.DownloadActivity
|
||||
import org.futo.voiceinput.shared.ENGLISH_MODELS
|
||||
import org.futo.voiceinput.shared.MULTILINGUAL_MODELS
|
||||
import org.futo.voiceinput.shared.ModelDoesNotExistException
|
||||
@ -111,6 +117,7 @@ private class VoiceInputActionWindow(
|
||||
}
|
||||
|
||||
private var recognizerView: MutableState<RecognizerView?> = mutableStateOf(null)
|
||||
private var modelException: MutableState<ModelDoesNotExistException?> = mutableStateOf(null)
|
||||
|
||||
private val initJob = manager.getLifecycleScope().launch {
|
||||
yield()
|
||||
@ -128,8 +135,7 @@ private class VoiceInputActionWindow(
|
||||
modelManager = state.modelManager
|
||||
)
|
||||
} catch(e: ModelDoesNotExistException) {
|
||||
// TODO: Show an error to the user, with an option to download
|
||||
close()
|
||||
modelException.value = e
|
||||
return@launch
|
||||
}
|
||||
|
||||
@ -151,6 +157,23 @@ private class VoiceInputActionWindow(
|
||||
return inputTransaction!!
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModelDownloader(modelException: ModelDoesNotExistException) {
|
||||
val context = LocalContext.current
|
||||
Box(modifier = Modifier.fillMaxSize().clickable {
|
||||
val intent = Intent(context, DownloadActivity::class.java)
|
||||
intent.putStringArrayListExtra("models", ArrayList(modelException.models.map { model -> model.getRequiredDownloadList(context) }.flatten()))
|
||||
|
||||
if(context !is Activity) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
context.startActivity(intent)
|
||||
}) {
|
||||
Text("Tap to complete setup", modifier = Modifier.align(Alignment.Center))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun windowName(): String {
|
||||
return stringResource(R.string.voice_input_action_title)
|
||||
@ -167,7 +190,10 @@ private class VoiceInputActionWindow(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() })) {
|
||||
Box(modifier = Modifier.align(Alignment.Center)) {
|
||||
recognizerView.value?.Content()
|
||||
when {
|
||||
modelException.value != null -> ModelDownloader(modelException.value!!)
|
||||
recognizerView.value != null -> recognizerView.value!!.Content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -178,12 +204,14 @@ private class VoiceInputActionWindow(
|
||||
}
|
||||
|
||||
private var wasFinished = false
|
||||
private var cancelPlayed = false
|
||||
override fun cancelled() {
|
||||
if (!wasFinished) {
|
||||
if (shouldPlaySounds) {
|
||||
if (shouldPlaySounds && !cancelPlayed) {
|
||||
state.soundPlayer.playCancelSound()
|
||||
cancelPlayed = true
|
||||
}
|
||||
getOrStartInputTransaction().cancel()
|
||||
inputTransaction?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
|
295
java/src/org/futo/inputmethod/latin/uix/settings/Components.kt
Normal file
@ -0,0 +1,295 @@
|
||||
package org.futo.inputmethod.latin.uix.settings
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ArrowForward
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.drawscope.translate
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import org.futo.inputmethod.latin.uix.SettingsKey
|
||||
import org.futo.inputmethod.latin.uix.theme.Typography
|
||||
|
||||
@Composable
|
||||
fun ScreenTitle(title: String, showBack: Boolean = false, navController: NavHostController = rememberNavController()) {
|
||||
val rowModifier = if(showBack) {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { navController.popBackStack() }
|
||||
} else {
|
||||
Modifier.fillMaxWidth()
|
||||
}
|
||||
Row(modifier = rowModifier) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
if(showBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back", modifier = Modifier.align(CenterVertically))
|
||||
Spacer(modifier = Modifier.width(18.dp))
|
||||
}
|
||||
Text(title, style = Typography.titleLarge, modifier = Modifier
|
||||
.align(CenterVertically)
|
||||
.padding(0.dp, 16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun Tip(text: String = "This is an example tip") {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.primaryContainer, modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp), shape = RoundedCornerShape(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text,
|
||||
modifier = Modifier.padding(8.dp),
|
||||
style = Typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun SettingItem(
|
||||
title: String,
|
||||
subtitle: String? = null,
|
||||
onClick: () -> Unit,
|
||||
icon: (@Composable () -> Unit)? = null,
|
||||
disabled: Boolean = false,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(0.dp, 68.dp)
|
||||
.clickable(enabled = !disabled, onClick = {
|
||||
if (!disabled) {
|
||||
onClick()
|
||||
}
|
||||
})
|
||||
.padding(0.dp, 4.dp, 0.dp, 4.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(48.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
) {
|
||||
Box(modifier = Modifier.align(Alignment.CenterHorizontally)) {
|
||||
if (icon != null) {
|
||||
icon()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterVertically)
|
||||
.alpha(
|
||||
if (disabled) {
|
||||
0.5f
|
||||
} else {
|
||||
1.0f
|
||||
}
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
Text(title, style = Typography.bodyLarge)
|
||||
|
||||
if (subtitle != null) {
|
||||
Text(
|
||||
subtitle,
|
||||
style = Typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(modifier = Modifier.align(Alignment.CenterVertically)) {
|
||||
content()
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingToggleRaw(
|
||||
title: String,
|
||||
enabled: Boolean,
|
||||
setValue: (Boolean) -> Unit,
|
||||
subtitle: String? = null,
|
||||
disabled: Boolean = false,
|
||||
icon: (@Composable () -> Unit)? = null
|
||||
) {
|
||||
SettingItem(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
onClick = {
|
||||
if (!disabled) {
|
||||
setValue(!enabled)
|
||||
}
|
||||
},
|
||||
icon = icon
|
||||
) {
|
||||
Switch(checked = enabled, onCheckedChange = {
|
||||
if (!disabled) {
|
||||
setValue(!enabled)
|
||||
}
|
||||
}, enabled = !disabled)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingToggleDataStoreItem(
|
||||
title: String,
|
||||
dataStoreItem: DataStoreItem<Boolean>,
|
||||
subtitle: String? = null,
|
||||
disabledSubtitle: String? = null,
|
||||
disabled: Boolean = false,
|
||||
icon: (@Composable () -> Unit)? = null
|
||||
) {
|
||||
val (enabled, setValue) = dataStoreItem
|
||||
|
||||
val subtitleValue = if (!enabled && disabledSubtitle != null) {
|
||||
disabledSubtitle
|
||||
} else {
|
||||
subtitle
|
||||
}
|
||||
|
||||
SettingToggleRaw(title, enabled, { setValue(it) }, subtitleValue, disabled, icon)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingToggleDataStore(
|
||||
title: String,
|
||||
setting: SettingsKey<Boolean>,
|
||||
subtitle: String? = null,
|
||||
disabledSubtitle: String? = null,
|
||||
disabled: Boolean = false,
|
||||
icon: (@Composable () -> Unit)? = null
|
||||
) {
|
||||
SettingToggleDataStoreItem(
|
||||
title, useDataStore(setting.key, setting.default), subtitle, disabledSubtitle, disabled, icon)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingToggleSharedPrefs(
|
||||
title: String,
|
||||
key: String,
|
||||
default: Boolean,
|
||||
subtitle: String? = null,
|
||||
disabledSubtitle: String? = null,
|
||||
disabled: Boolean = false,
|
||||
icon: (@Composable () -> Unit)? = null
|
||||
) {
|
||||
SettingToggleDataStoreItem(
|
||||
title, useSharedPrefsBool(key, default), subtitle, disabledSubtitle, disabled, icon)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScrollableList(content: @Composable () -> Unit) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingListLazy(content: LazyListScope.() -> Unit) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum class NavigationItemStyle {
|
||||
HomePrimary,
|
||||
HomeSecondary,
|
||||
HomeTertiary,
|
||||
Misc
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavigationItem(title: String, style: NavigationItemStyle, navigate: () -> Unit, icon: Painter? = null) {
|
||||
SettingItem(
|
||||
title = title,
|
||||
onClick = navigate,
|
||||
icon = {
|
||||
icon?.let {
|
||||
val circleColor = when(style) {
|
||||
NavigationItemStyle.HomePrimary -> MaterialTheme.colorScheme.primaryContainer
|
||||
NavigationItemStyle.HomeSecondary -> MaterialTheme.colorScheme.secondaryContainer
|
||||
NavigationItemStyle.HomeTertiary -> MaterialTheme.colorScheme.tertiaryContainer
|
||||
NavigationItemStyle.Misc -> Color.Transparent
|
||||
}
|
||||
|
||||
val iconColor = when(style) {
|
||||
NavigationItemStyle.HomePrimary -> MaterialTheme.colorScheme.onPrimaryContainer
|
||||
NavigationItemStyle.HomeSecondary -> MaterialTheme.colorScheme.onSecondaryContainer
|
||||
NavigationItemStyle.HomeTertiary -> MaterialTheme.colorScheme.onTertiaryContainer
|
||||
NavigationItemStyle.Misc -> MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f)
|
||||
}
|
||||
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
drawCircle(circleColor, this.size.maxDimension / 2.4f)
|
||||
translate(
|
||||
left = this.size.width / 2.0f - icon.intrinsicSize.width / 2.0f,
|
||||
top = this.size.height / 2.0f - icon.intrinsicSize.height / 2.0f
|
||||
) {
|
||||
with(icon) {
|
||||
draw(icon.intrinsicSize, colorFilter = ColorFilter.tint(iconColor))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
when(style) {
|
||||
NavigationItemStyle.Misc -> Icon(Icons.Default.ArrowForward, contentDescription = "Go")
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
88
java/src/org/futo/inputmethod/latin/uix/settings/Hooks.kt
Normal file
@ -0,0 +1,88 @@
|
||||
package org.futo.inputmethod.latin.uix.settings
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.preference.PreferenceManager
|
||||
import android.provider.Settings
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.edit
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.futo.inputmethod.latin.uix.dataStore
|
||||
|
||||
data class DataStoreItem<T>(val value: T, val setValue: (T) -> Job)
|
||||
@Composable
|
||||
fun <T> useDataStore(key: Preferences.Key<T>, default: T): DataStoreItem<T> {
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val enableSoundFlow: Flow<T> = remember {
|
||||
context.dataStore.data.map {
|
||||
preferences -> preferences[key] ?: default
|
||||
}
|
||||
}
|
||||
|
||||
val value = enableSoundFlow.collectAsState(initial = default).value!!
|
||||
|
||||
val setValue = { newValue: T ->
|
||||
coroutineScope.launch {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[key] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DataStoreItem(value, setValue)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun useSharedPrefsBool(key: String, default: Boolean): DataStoreItem<Boolean> {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val sharedPrefs = remember { PreferenceManager.getDefaultSharedPreferences(context) }
|
||||
|
||||
val value = remember { mutableStateOf(sharedPrefs.getBoolean(key, default)) }
|
||||
|
||||
// This is not the most efficient way to do this... but it works for a settings menu
|
||||
DisposableEffect(Unit) {
|
||||
val listener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, changedKey ->
|
||||
if (key == changedKey) {
|
||||
value.value = sharedPreferences.getBoolean(key, value.value)
|
||||
}
|
||||
}
|
||||
|
||||
sharedPrefs.registerOnSharedPreferenceChangeListener(listener)
|
||||
|
||||
onDispose {
|
||||
sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
val setValue = { newValue: Boolean ->
|
||||
coroutineScope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
sharedPrefs.edit {
|
||||
putBoolean(key, newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DataStoreItem(value.value, setValue)
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
package org.futo.inputmethod.latin.uix.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Context.INPUT_METHOD_SERVICE
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.futo.inputmethod.latin.uix.THEME_KEY
|
||||
import org.futo.inputmethod.latin.uix.deferGetSetting
|
||||
import org.futo.inputmethod.latin.uix.theme.StatusBarColorSetter
|
||||
import org.futo.inputmethod.latin.uix.theme.ThemeOption
|
||||
import org.futo.inputmethod.latin.uix.theme.ThemeOptions
|
||||
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
|
||||
import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme
|
||||
|
||||
private fun Context.isInputMethodEnabled(): Boolean {
|
||||
val packageName = packageName
|
||||
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
|
||||
var found = false
|
||||
for (imi in imm.enabledInputMethodList) {
|
||||
if (packageName == imi.packageName) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
private fun Context.isDefaultIMECurrent(): Boolean {
|
||||
val value = Settings.Secure.getString(contentResolver, Settings.Secure.DEFAULT_INPUT_METHOD)
|
||||
|
||||
return value.startsWith(packageName)
|
||||
}
|
||||
|
||||
|
||||
class SettingsActivity : ComponentActivity() {
|
||||
private val themeOption: MutableState<ThemeOption?> = mutableStateOf(null)
|
||||
|
||||
private val inputMethodEnabled = mutableStateOf(false)
|
||||
private val inputMethodSelected = mutableStateOf(false)
|
||||
|
||||
private var wasImeEverDisabled = false
|
||||
|
||||
companion object {
|
||||
private var pollJob: Job? = null
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun updateSystemState() {
|
||||
val inputMethodEnabled = isInputMethodEnabled()
|
||||
val inputMethodSelected = isDefaultIMECurrent()
|
||||
this.inputMethodEnabled.value = inputMethodEnabled
|
||||
this.inputMethodSelected.value = inputMethodSelected
|
||||
|
||||
if(!inputMethodEnabled) {
|
||||
wasImeEverDisabled = true
|
||||
} else if(wasImeEverDisabled) {
|
||||
// We just went from inputMethodEnabled==false to inputMethodEnabled==true
|
||||
// This is because the user is in the input method settings screen and just turned on
|
||||
// our IME. We can bring them back here so that they don't have to press back button
|
||||
wasImeEverDisabled = false
|
||||
|
||||
val intent = Intent()
|
||||
intent.setClass(this, SettingsActivity::class.java)
|
||||
intent.flags = (Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
|
||||
or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
if(!inputMethodEnabled || !inputMethodSelected) {
|
||||
if(pollJob == null || !pollJob!!.isActive) {
|
||||
pollJob = GlobalScope.launch {
|
||||
systemStatePoller()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun systemStatePoller() {
|
||||
while(!this.inputMethodEnabled.value || !this.inputMethodSelected.value) {
|
||||
delay(200)
|
||||
updateSystemState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateContent() {
|
||||
setContent {
|
||||
themeOption.value?.let { themeOption ->
|
||||
val themeIdx = useDataStore(key = THEME_KEY.key, default = themeOption.key)
|
||||
val theme: ThemeOption = ThemeOptions[themeIdx.value] ?: themeOption
|
||||
UixThemeWrapper(theme.obtainColors(LocalContext.current)) {
|
||||
StatusBarColorSetter()
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
SetupOrMain(inputMethodEnabled.value, inputMethodSelected.value) {
|
||||
SettingsNavigator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
updateSystemState()
|
||||
updateContent()
|
||||
}
|
||||
}
|
||||
|
||||
deferGetSetting(THEME_KEY) {
|
||||
val themeOptionFromSettings = ThemeOptions[it]
|
||||
val themeOption = when {
|
||||
themeOptionFromSettings == null -> VoiceInputTheme
|
||||
!themeOptionFromSettings.available(this) -> VoiceInputTheme
|
||||
else -> themeOptionFromSettings
|
||||
}
|
||||
|
||||
this.themeOption.value = themeOption
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
updateSystemState()
|
||||
}
|
||||
|
||||
override fun onRestart() {
|
||||
super.onRestart()
|
||||
|
||||
updateSystemState()
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package org.futo.inputmethod.latin.uix.settings
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import org.futo.inputmethod.latin.uix.settings.pages.HomeScreen
|
||||
import org.futo.inputmethod.latin.uix.settings.pages.PredictiveTextScreen
|
||||
import org.futo.inputmethod.latin.uix.settings.pages.ThemeScreen
|
||||
import org.futo.inputmethod.latin.uix.settings.pages.TypingScreen
|
||||
import org.futo.inputmethod.latin.uix.settings.pages.VoiceInputScreen
|
||||
|
||||
@Composable
|
||||
fun SettingsNavigator(
|
||||
navController: NavHostController = rememberNavController()
|
||||
) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = "home"
|
||||
) {
|
||||
composable("home") { HomeScreen(navController) }
|
||||
composable("predictiveText") { PredictiveTextScreen(navController) }
|
||||
composable("typing") { TypingScreen(navController) }
|
||||
composable("voiceInput") { VoiceInputScreen(navController) }
|
||||
composable("themes") { ThemeScreen(navController) }
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package org.futo.inputmethod.latin.uix.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.futo.inputmethod.latin.utils.UncachedInputMethodManagerUtils
|
||||
|
||||
@Composable
|
||||
fun SetupOrMain(inputMethodEnabled: Boolean, inputMethodSelected: Boolean, main: @Composable () -> Unit) {
|
||||
if (!inputMethodEnabled) {
|
||||
SetupEnableIME()
|
||||
} else if (!inputMethodSelected) {
|
||||
SetupChangeDefaultIME()
|
||||
} else {
|
||||
main()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We should have one central source of enabled languages to share between
|
||||
// keyboard and voice input. We need to pass current language to voice input action
|
||||
// and restrict it to that when possible. If active language has no voice input support
|
||||
// we must tell the user in the UI.
|
||||
fun Context.openLanguageSettings() {
|
||||
val imm = getSystemService(ComponentActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
|
||||
val imi = UncachedInputMethodManagerUtils.getInputMethodInfoOf(
|
||||
packageName, imm
|
||||
) ?: return
|
||||
val intent = Intent()
|
||||
intent.action = Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS
|
||||
intent.addCategory(Intent.CATEGORY_DEFAULT)
|
||||
intent.putExtra(Settings.EXTRA_INPUT_METHOD_ID, imi.id)
|
||||
startActivity(intent)
|
||||
}
|
147
java/src/org/futo/inputmethod/latin/uix/settings/Setup.kt
Normal file
@ -0,0 +1,147 @@
|
||||
package org.futo.inputmethod.latin.uix.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.futo.inputmethod.latin.R
|
||||
import org.futo.inputmethod.latin.uix.theme.Typography
|
||||
|
||||
@Composable
|
||||
fun SetupContainer(inner: @Composable () -> Unit) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fraction = 1.0f)
|
||||
.fillMaxHeight(fraction = 0.4f)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.futo_logo),
|
||||
contentDescription = "FUTO Logo",
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.fillMaxWidth(0.75f)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(fraction = 0.5f)
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(32.dp)
|
||||
) {
|
||||
Box(modifier = Modifier.align(Alignment.CenterVertically)) {
|
||||
inner()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun Step(fraction: Float, text: String) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(text, style = Typography.labelSmall)
|
||||
LinearProgressIndicator(progress = fraction, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: May wish to have a skip option
|
||||
@Composable
|
||||
@Preview
|
||||
fun SetupEnableIME() {
|
||||
val context = LocalContext.current
|
||||
|
||||
val launchImeOptions = {
|
||||
// TODO: look into direct boot to get rid of direct boot warning?
|
||||
val intent = Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)
|
||||
|
||||
intent.flags = (Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
|
||||
or Intent.FLAG_ACTIVITY_NO_HISTORY
|
||||
or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
|
||||
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
SetupContainer {
|
||||
Column {
|
||||
Step(fraction = 1.0f/3.0f, text = "Setup - Step 1 of 2")
|
||||
|
||||
Text(
|
||||
"To use FUTO Keyboard, you must first enable FUTO Keyboard as an input method.",
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Button(
|
||||
onClick = launchImeOptions,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text("Open Input Method Settings")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun SetupChangeDefaultIME() {
|
||||
val context = LocalContext.current
|
||||
|
||||
val launchImeOptions = {
|
||||
val inputMethodManager =
|
||||
context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
|
||||
inputMethodManager.showInputMethodPicker()
|
||||
}
|
||||
|
||||
SetupContainer {
|
||||
Column {
|
||||
Step(fraction = 2.0f/3.0f, text = "Setup - Step 2 of 2")
|
||||
|
||||
Text(
|
||||
"Next, select FUTO Keyboard as your active input method.",
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Button(
|
||||
onClick = launchImeOptions,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text("Switch Input Methods")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package org.futo.inputmethod.latin.uix.settings.pages
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import org.futo.inputmethod.latin.R
|
||||
import org.futo.inputmethod.latin.uix.settings.NavigationItem
|
||||
import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle
|
||||
import org.futo.inputmethod.latin.uix.settings.ScreenTitle
|
||||
import org.futo.inputmethod.latin.uix.settings.ScrollableList
|
||||
import org.futo.inputmethod.latin.uix.settings.openLanguageSettings
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun HomeScreen(navController: NavHostController = rememberNavController()) {
|
||||
val context = LocalContext.current
|
||||
ScrollableList {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
ScreenTitle("FUTO Keyboard Settings")
|
||||
NavigationItem(
|
||||
title = "Languages",
|
||||
style = NavigationItemStyle.HomePrimary,
|
||||
navigate = { context.openLanguageSettings() },
|
||||
icon = painterResource(id = R.drawable.globe)
|
||||
)
|
||||
|
||||
NavigationItem(
|
||||
title = "Predictive Text",
|
||||
style = NavigationItemStyle.HomeSecondary,
|
||||
navigate = { navController.navigate("predictiveText") },
|
||||
icon = painterResource(id = R.drawable.shift)
|
||||
)
|
||||
|
||||
NavigationItem(
|
||||
title = "Typing Preferences",
|
||||
style = NavigationItemStyle.HomeSecondary,
|
||||
navigate = { navController.navigate("typing") },
|
||||
icon = painterResource(id = R.drawable.delete)
|
||||
)
|
||||
|
||||
NavigationItem(
|
||||
title = "Voice Input",
|
||||
style = NavigationItemStyle.HomeSecondary,
|
||||
navigate = { navController.navigate("voiceInput") },
|
||||
icon = painterResource(id = R.drawable.mic_fill)
|
||||
)
|
||||
|
||||
NavigationItem(
|
||||
title = "Theme",
|
||||
style = NavigationItemStyle.HomeTertiary,
|
||||
navigate = { navController.navigate("themes") },
|
||||
icon = painterResource(id = R.drawable.eye)
|
||||
)
|
||||
|
||||
/*
|
||||
NavigationItem(
|
||||
title = "Advanced",
|
||||
style = NavigationItemStyle.Misc,
|
||||
navigate = { },
|
||||
icon = painterResource(id = R.drawable.delete)
|
||||
)
|
||||
*/
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
package org.futo.inputmethod.latin.uix.settings.pages
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.booleanResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import org.futo.inputmethod.dictionarypack.DictionarySettingsActivity
|
||||
import org.futo.inputmethod.latin.R
|
||||
import org.futo.inputmethod.latin.settings.Settings
|
||||
import org.futo.inputmethod.latin.uix.settings.NavigationItem
|
||||
import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle
|
||||
import org.futo.inputmethod.latin.uix.settings.ScreenTitle
|
||||
import org.futo.inputmethod.latin.uix.settings.ScrollableList
|
||||
import org.futo.inputmethod.latin.uix.settings.SettingToggleSharedPrefs
|
||||
import org.futo.inputmethod.latin.uix.settings.Tip
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PredictiveTextScreen(navController: NavHostController = rememberNavController()) {
|
||||
val context = LocalContext.current
|
||||
ScrollableList {
|
||||
ScreenTitle("Predictive Text", showBack = true, navController)
|
||||
|
||||
Tip("Note: Transformer LM is not yet finished, the prediction algorithm is still the default AOSP Keyboard prediction algorithm")
|
||||
|
||||
NavigationItem(
|
||||
title = stringResource(R.string.edit_personal_dictionary),
|
||||
style = NavigationItemStyle.Misc,
|
||||
navigate = {
|
||||
val intent = Intent("android.settings.USER_DICTIONARY_SETTINGS")
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
)
|
||||
|
||||
NavigationItem(
|
||||
title = stringResource(R.string.configure_dictionaries_title),
|
||||
style = NavigationItemStyle.Misc,
|
||||
navigate = {
|
||||
val intent = Intent()
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
intent.setClass(context, DictionarySettingsActivity::class.java)
|
||||
intent.putExtra("clientId", "org.futo.inputmethod.latin")
|
||||
context.startActivity(intent)
|
||||
}
|
||||
)
|
||||
|
||||
SettingToggleSharedPrefs(
|
||||
title = stringResource(R.string.prefs_block_potentially_offensive_title),
|
||||
subtitle = stringResource(R.string.prefs_block_potentially_offensive_summary),
|
||||
key = Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE,
|
||||
default = booleanResource(R.bool.config_block_potentially_offensive)
|
||||
)
|
||||
SettingToggleSharedPrefs(
|
||||
title = stringResource(R.string.auto_correction),
|
||||
subtitle = stringResource(R.string.auto_correction_summary),
|
||||
key = Settings.PREF_AUTO_CORRECTION,
|
||||
default = true
|
||||
)
|
||||
SettingToggleSharedPrefs(
|
||||
title = stringResource(R.string.prefs_show_suggestions),
|
||||
subtitle = stringResource(R.string.prefs_show_suggestions_summary),
|
||||
key = Settings.PREF_SHOW_SUGGESTIONS,
|
||||
default = true
|
||||
)
|
||||
SettingToggleSharedPrefs(
|
||||
title = stringResource(R.string.use_personalized_dicts),
|
||||
subtitle = stringResource(R.string.use_personalized_dicts_summary),
|
||||
key = Settings.PREF_KEY_USE_PERSONALIZED_DICTS,
|
||||
default = true
|
||||
)
|
||||
SettingToggleSharedPrefs(
|
||||
title = stringResource(R.string.bigram_prediction),
|
||||
subtitle = stringResource(R.string.bigram_prediction_summary),
|
||||
key = Settings.PREF_BIGRAM_PREDICTIONS,
|
||||
default = booleanResource(R.bool.config_default_next_word_prediction)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package org.futo.inputmethod.latin.uix.settings.pages
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import org.futo.inputmethod.latin.uix.THEME_KEY
|
||||
import org.futo.inputmethod.latin.uix.settings.ScreenTitle
|
||||
import org.futo.inputmethod.latin.uix.settings.useDataStore
|
||||
import org.futo.inputmethod.latin.uix.theme.selector.ThemePicker
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ThemeScreen(navController: NavHostController = rememberNavController()) {
|
||||
val (theme, setTheme) = useDataStore(THEME_KEY.key, THEME_KEY.default)
|
||||
|
||||
val context = LocalContext.current
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
ScreenTitle("Theme", showBack = true, navController)
|
||||
ThemePicker {
|
||||
setTheme(it.key)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package org.futo.inputmethod.latin.uix.settings.pages
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.booleanResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import org.futo.inputmethod.latin.R
|
||||
import org.futo.inputmethod.latin.settings.Settings
|
||||
import org.futo.inputmethod.latin.uix.settings.ScreenTitle
|
||||
import org.futo.inputmethod.latin.uix.settings.ScrollableList
|
||||
import org.futo.inputmethod.latin.uix.settings.SettingToggleSharedPrefs
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun TypingScreen(navController: NavHostController = rememberNavController()) {
|
||||
val context = LocalContext.current
|
||||
ScrollableList {
|
||||
ScreenTitle("Typing Preferences", showBack = true, navController)
|
||||
|
||||
SettingToggleSharedPrefs(
|
||||
title = stringResource(R.string.auto_cap),
|
||||
subtitle = stringResource(R.string.auto_cap_summary),
|
||||
key = Settings.PREF_AUTO_CAP,
|
||||
default = true
|
||||
)
|
||||
SettingToggleSharedPrefs(
|
||||
title = stringResource(R.string.use_double_space_period),
|
||||
subtitle = stringResource(R.string.use_double_space_period_summary),
|
||||
key = Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD,
|
||||
default = true
|
||||
)
|
||||
SettingToggleSharedPrefs(
|
||||
title = stringResource(R.string.vibrate_on_keypress),
|
||||
key = Settings.PREF_VIBRATE_ON,
|
||||
default = booleanResource(R.bool.config_default_vibration_enabled)
|
||||
)
|
||||
SettingToggleSharedPrefs(
|
||||
title = stringResource(R.string.sound_on_keypress),
|
||||
key = Settings.PREF_SOUND_ON,
|
||||
default = booleanResource(R.bool.config_default_sound_enabled)
|
||||
)
|
||||
SettingToggleSharedPrefs(
|
||||
title = stringResource(R.string.popup_on_keypress),
|
||||
key = Settings.PREF_POPUP_ON,
|
||||
default = booleanResource(R.bool.config_default_key_preview_popup)
|
||||
)
|
||||
|
||||
// TODO: SeekBarDialogPreference pref_vibration_duration_settings etc
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
package org.futo.inputmethod.latin.uix.settings.pages
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import org.futo.inputmethod.latin.uix.DISALLOW_SYMBOLS
|
||||
import org.futo.inputmethod.latin.uix.ENABLE_SOUND
|
||||
import org.futo.inputmethod.latin.uix.ENGLISH_MODEL_INDEX
|
||||
import org.futo.inputmethod.latin.uix.SettingsKey
|
||||
import org.futo.inputmethod.latin.uix.VERBOSE_PROGRESS
|
||||
import org.futo.inputmethod.latin.uix.settings.ScreenTitle
|
||||
import org.futo.inputmethod.latin.uix.settings.ScrollableList
|
||||
import org.futo.inputmethod.latin.uix.settings.SettingToggleDataStore
|
||||
import org.futo.inputmethod.latin.uix.settings.useDataStore
|
||||
import org.futo.voiceinput.shared.ENGLISH_MODELS
|
||||
import org.futo.voiceinput.shared.types.ModelLoader
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ModelPicker(label: String, options: List<ModelLoader>, setting: SettingsKey<Int>) {
|
||||
val (modelIndex, setModelIndex) = useDataStore(key = setting.key, default = setting.default)
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = {
|
||||
expanded = !expanded
|
||||
},
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
) {
|
||||
TextField(
|
||||
readOnly = true,
|
||||
value = stringResource(options[modelIndex].name),
|
||||
onValueChange = { },
|
||||
label = { Text(label) },
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(
|
||||
expanded = expanded
|
||||
)
|
||||
},
|
||||
colors = ExposedDropdownMenuDefaults.textFieldColors(
|
||||
focusedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
focusedLeadingIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
focusedIndicatorColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
focusedTrailingIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
modifier = Modifier.menuAnchor()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = {
|
||||
expanded = false
|
||||
}
|
||||
) {
|
||||
options.forEachIndexed { i, selectionOption ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(stringResource(selectionOption.name))
|
||||
},
|
||||
onClick = {
|
||||
setModelIndex(i)
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun VoiceInputScreen(navController: NavHostController = rememberNavController()) {
|
||||
val context = LocalContext.current
|
||||
ScrollableList {
|
||||
ScreenTitle("Voice Input", showBack = true, navController)
|
||||
|
||||
SettingToggleDataStore(
|
||||
title = "Indication sounds",
|
||||
subtitle = "Play sounds on start and cancel",
|
||||
setting = ENABLE_SOUND
|
||||
)
|
||||
|
||||
SettingToggleDataStore(
|
||||
title = "Verbose progress",
|
||||
subtitle = "Display verbose information about model inference",
|
||||
setting = VERBOSE_PROGRESS
|
||||
)
|
||||
|
||||
SettingToggleDataStore(
|
||||
title = "Suppress symbols",
|
||||
setting = DISALLOW_SYMBOLS
|
||||
)
|
||||
|
||||
ModelPicker(
|
||||
"English Model Option",
|
||||
ENGLISH_MODELS,
|
||||
ENGLISH_MODEL_INDEX
|
||||
)
|
||||
}
|
||||
}
|
@ -1,14 +1,18 @@
|
||||
package org.futo.inputmethod.latin.uix.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.view.WindowManager
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
@ -45,6 +49,25 @@ val DarkColorScheme = darkColorScheme(
|
||||
onSurfaceVariant = Slate300
|
||||
)
|
||||
|
||||
fun applyWindowColors(context: Context, backgroundColor: Color) {
|
||||
val window = (context as Activity).window
|
||||
val color = backgroundColor.copy(alpha = 0.75f).toArgb()
|
||||
|
||||
window.statusBarColor = color
|
||||
window.navigationBarColor = color
|
||||
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StatusBarColorSetter() {
|
||||
val backgroundColor = MaterialTheme.colorScheme.background
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(backgroundColor) {
|
||||
applyWindowColors(context, backgroundColor)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UixThemeWrapper(colorScheme: ColorScheme, content: @Composable () -> Unit) {
|
||||
MaterialTheme(
|
||||
|
@ -5,6 +5,7 @@ import androidx.annotation.StringRes
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import org.futo.inputmethod.latin.uix.theme.presets.AMOLEDDarkPurple
|
||||
import org.futo.inputmethod.latin.uix.theme.presets.ClassicMaterialDark
|
||||
import org.futo.inputmethod.latin.uix.theme.presets.ClassicMaterialLight
|
||||
import org.futo.inputmethod.latin.uix.theme.presets.DynamicDarkTheme
|
||||
import org.futo.inputmethod.latin.uix.theme.presets.DynamicLightTheme
|
||||
import org.futo.inputmethod.latin.uix.theme.presets.DynamicSystemTheme
|
||||
@ -24,16 +25,18 @@ val ThemeOptions = hashMapOf(
|
||||
DynamicLightTheme.key to DynamicLightTheme,
|
||||
|
||||
ClassicMaterialDark.key to ClassicMaterialDark,
|
||||
ClassicMaterialLight.key to ClassicMaterialLight,
|
||||
VoiceInputTheme.key to VoiceInputTheme,
|
||||
AMOLEDDarkPurple.key to AMOLEDDarkPurple,
|
||||
)
|
||||
|
||||
val ThemeOptionKeys = arrayOf(
|
||||
DynamicSystemTheme.key,
|
||||
VoiceInputTheme.key,
|
||||
DynamicDarkTheme.key,
|
||||
DynamicLightTheme.key,
|
||||
DynamicSystemTheme.key,
|
||||
|
||||
ClassicMaterialDark.key,
|
||||
VoiceInputTheme.key,
|
||||
ClassicMaterialLight.key,
|
||||
AMOLEDDarkPurple.key,
|
||||
)
|
@ -1,9 +1,12 @@
|
||||
package org.futo.inputmethod.latin.uix.theme.presets
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import org.futo.inputmethod.latin.R
|
||||
import org.futo.inputmethod.latin.uix.theme.ThemeOption
|
||||
import org.futo.inputmethod.latin.uix.theme.selector.ThemePreview
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFFD0BCFF)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF381E72)
|
||||
@ -75,4 +78,10 @@ val AMOLEDDarkPurple = ThemeOption(
|
||||
available = { true }
|
||||
) {
|
||||
colorScheme
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun PreviewTheme() {
|
||||
ThemePreview(AMOLEDDarkPurple)
|
||||
}
|
@ -20,6 +20,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.futo.inputmethod.latin.R
|
||||
import org.futo.inputmethod.latin.uix.theme.ThemeOption
|
||||
import org.futo.inputmethod.latin.uix.theme.selector.ThemePreview
|
||||
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFF80cbc4)
|
||||
@ -28,11 +29,11 @@ private val md_theme_dark_primaryContainer = Color(0xFF34434B)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFFF0FFFE)
|
||||
private val md_theme_dark_secondary = Color(0xFF80cbc4)
|
||||
private val md_theme_dark_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF34434B)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF416152)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFFFFFFF)
|
||||
private val md_theme_dark_tertiary = Color(0xFF3582A2)
|
||||
private val md_theme_dark_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF004D64)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF17516D)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFBDE9FF)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
@ -97,31 +98,5 @@ val ClassicMaterialDark = ThemeOption(
|
||||
@Composable
|
||||
@Preview
|
||||
private fun PreviewTheme() {
|
||||
MaterialTheme(colorScheme) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Spacer(modifier = Modifier.weight(1.5f))
|
||||
Surface(color = MaterialTheme.colorScheme.background, modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)) {
|
||||
|
||||
}
|
||||
Surface(color = MaterialTheme.colorScheme.surface, modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1.0f)) {
|
||||
Box(modifier = Modifier.padding(16.dp)) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.primary, modifier = Modifier
|
||||
.align(
|
||||
Alignment.BottomEnd
|
||||
)
|
||||
.height(32.dp)
|
||||
.width(48.dp),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ThemePreview(ClassicMaterialDark)
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
package org.futo.inputmethod.latin.uix.theme.presets
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.futo.inputmethod.latin.R
|
||||
import org.futo.inputmethod.latin.uix.theme.ThemeOption
|
||||
import org.futo.inputmethod.latin.uix.theme.selector.ThemePreview
|
||||
|
||||
|
||||
|
||||
val md_theme_light_primary = Color(0xFF4db6ac)
|
||||
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_primaryContainer = Color(0xFFCCE8E4)
|
||||
val md_theme_light_onPrimaryContainer = Color(0xFF00201D)
|
||||
val md_theme_light_secondary = Color(0xFF4A6360)
|
||||
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_secondaryContainer = Color(0xFFD8E8CC)
|
||||
val md_theme_light_onSecondaryContainer = Color(0xFF051F1D)
|
||||
val md_theme_light_tertiary = Color(0xFF47617A)
|
||||
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_tertiaryContainer = Color(0xFFCEEBFF)
|
||||
val md_theme_light_onTertiaryContainer = Color(0xFF001D33)
|
||||
val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
val md_theme_light_background = Color(0xFFe4e7e9)
|
||||
val md_theme_light_onBackground = Color(0xFF191C1C)
|
||||
val md_theme_light_surface = Color(0xFFeceff1)
|
||||
val md_theme_light_onSurface = Color(0xFF191C1C)
|
||||
val md_theme_light_surfaceVariant = Color(0xFFDAE5E2)
|
||||
val md_theme_light_onSurfaceVariant = Color(0xFF3F4947)
|
||||
val md_theme_light_outline = Color(0xFF9EA2A7)
|
||||
val md_theme_light_inverseOnSurface = Color(0xFFEFF1F0)
|
||||
val md_theme_light_inverseSurface = Color(0xFF2D3130)
|
||||
val md_theme_light_inversePrimary = Color(0xFF50DBCE)
|
||||
val md_theme_light_shadow = Color(0xFF000000)
|
||||
val md_theme_light_surfaceTint = Color(0xFF006A63)
|
||||
val md_theme_light_outlineVariant = Color(0xFFBEC9C6)
|
||||
val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val colorScheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val ClassicMaterialLight = ThemeOption(
|
||||
dynamic = false,
|
||||
key = "ClassicMaterialLight",
|
||||
name = R.string.classic_material_light_theme_name,
|
||||
available = { true }
|
||||
) {
|
||||
colorScheme
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun PreviewTheme() {
|
||||
ThemePreview(ClassicMaterialLight)
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
package org.futo.inputmethod.latin.uix.theme.presets
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import org.futo.inputmethod.latin.R
|
||||
import org.futo.inputmethod.latin.uix.theme.DarkColorScheme
|
||||
import org.futo.inputmethod.latin.uix.theme.ThemeOption
|
||||
import org.futo.inputmethod.latin.uix.theme.selector.ThemePreview
|
||||
|
||||
val VoiceInputTheme = ThemeOption(
|
||||
dynamic = false,
|
||||
@ -11,4 +14,10 @@ val VoiceInputTheme = ThemeOption(
|
||||
available = { true }
|
||||
) {
|
||||
DarkColorScheme
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun PreviewTheme() {
|
||||
ThemePreview(VoiceInputTheme)
|
||||
}
|
@ -0,0 +1,226 @@
|
||||
package org.futo.inputmethod.latin.uix.theme.selector
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.futo.inputmethod.latin.uix.THEME_KEY
|
||||
import org.futo.inputmethod.latin.uix.settings.useDataStore
|
||||
import org.futo.inputmethod.latin.uix.theme.ThemeOption
|
||||
import org.futo.inputmethod.latin.uix.theme.ThemeOptionKeys
|
||||
import org.futo.inputmethod.latin.uix.theme.ThemeOptions
|
||||
import org.futo.inputmethod.latin.uix.theme.Typography
|
||||
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
|
||||
import org.futo.inputmethod.latin.uix.theme.presets.AMOLEDDarkPurple
|
||||
import org.futo.inputmethod.latin.uix.theme.presets.ClassicMaterialDark
|
||||
import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme
|
||||
|
||||
// TODO: For Dynamic System we need to show the user that it switches between light/dark
|
||||
@Composable
|
||||
fun ThemePreview(theme: ThemeOption, isSelected: Boolean = false, onClick: () -> Unit = { }) {
|
||||
val context = LocalContext.current
|
||||
val colors = remember { theme.obtainColors(context) }
|
||||
|
||||
val currColors = MaterialTheme.colorScheme
|
||||
|
||||
val borderWidth = if (isSelected) {
|
||||
2.dp
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
|
||||
val borderColor = if (isSelected) {
|
||||
currColors.inversePrimary
|
||||
} else {
|
||||
Color.Transparent
|
||||
}
|
||||
|
||||
val textColor = colors.onBackground
|
||||
|
||||
// TODO: These have to be manually kept same as those in BasicThemeProvider
|
||||
val spacebarColor = colors.outline.copy(alpha = 0.33f)
|
||||
val actionColor = colors.primary
|
||||
|
||||
val keyboardShape = RoundedCornerShape(8.dp)
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.width(172.dp)
|
||||
.height(128.dp)
|
||||
.border(borderWidth, borderColor, keyboardShape)
|
||||
.clickable { onClick() },
|
||||
color = colors.surface,
|
||||
shape = keyboardShape
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
// Theme name and action bar
|
||||
Text(
|
||||
text = stringResource(theme.name),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.background(colors.background)
|
||||
.fillMaxWidth()
|
||||
.padding(4.dp),
|
||||
color = textColor,
|
||||
style = Typography.labelSmall
|
||||
)
|
||||
|
||||
// Keyboard contents
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
// Spacebar
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.5f)
|
||||
.height(18.dp)
|
||||
.align(Alignment.BottomCenter),
|
||||
color = spacebarColor,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) { }
|
||||
|
||||
// Enter key
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.width(24.dp)
|
||||
.height(18.dp)
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(0.dp, 1.dp),
|
||||
color = actionColor,
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddCustomThemeButton(onClick: () -> Unit = { }) {
|
||||
val context = LocalContext.current
|
||||
val currColors = MaterialTheme.colorScheme
|
||||
|
||||
val keyboardShape = RoundedCornerShape(8.dp)
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.width(172.dp)
|
||||
.height(128.dp)
|
||||
.clickable { onClick() },
|
||||
color = currColors.surfaceVariant,
|
||||
shape = keyboardShape
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Icon(
|
||||
Icons.Default.Add, contentDescription = "", modifier = Modifier
|
||||
.size(48.dp)
|
||||
.align(
|
||||
Alignment.Center
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ThemePicker(onSelected: (ThemeOption) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val currentTheme = useDataStore(THEME_KEY.key, "").value
|
||||
|
||||
val isInspecting = LocalInspectionMode.current
|
||||
val availableThemeOptions = remember {
|
||||
ThemeOptionKeys.mapNotNull { key ->
|
||||
ThemeOptions[key]?.let { Pair(key, it) }
|
||||
}.filter {
|
||||
it.second.available(context)
|
||||
}.filter {
|
||||
when (isInspecting) {
|
||||
true -> !it.second.dynamic
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyVerticalGrid(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
columns = GridCells.Adaptive(minSize = 172.dp)
|
||||
) {
|
||||
items(availableThemeOptions.count()) {
|
||||
val themeOption = availableThemeOptions[it].second
|
||||
|
||||
ThemePreview(themeOption, isSelected = themeOption.key == currentTheme) {
|
||||
onSelected(themeOption)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
AddCustomThemeButton {
|
||||
// TODO: Custom themes
|
||||
val toast = Toast.makeText(context, "Custom themes coming eventually", Toast.LENGTH_SHORT)
|
||||
toast.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ThemePickerPreview() {
|
||||
Column {
|
||||
UixThemeWrapper(VoiceInputTheme.obtainColors(LocalContext.current)) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
ThemePicker {}
|
||||
}
|
||||
}
|
||||
UixThemeWrapper(ClassicMaterialDark.obtainColors(LocalContext.current)) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
ThemePicker {}
|
||||
}
|
||||
}
|
||||
UixThemeWrapper(AMOLEDDarkPurple.obtainColors(LocalContext.current)) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
ThemePicker {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,431 @@
|
||||
package org.futo.inputmethod.latin.uix.voiceinput.downloader
|
||||
|
||||
// TODO: Rework this
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.futo.inputmethod.latin.R
|
||||
import org.futo.inputmethod.latin.uix.THEME_KEY
|
||||
import org.futo.inputmethod.latin.uix.deferGetSetting
|
||||
import org.futo.inputmethod.latin.uix.settings.useDataStore
|
||||
import org.futo.inputmethod.latin.uix.theme.ThemeOption
|
||||
import org.futo.inputmethod.latin.uix.theme.ThemeOptions
|
||||
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
|
||||
import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme
|
||||
import org.futo.voiceinput.shared.ui.theme.Typography
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
data class ModelInfo(
|
||||
val name: String,
|
||||
val url: String,
|
||||
var size: Long?,
|
||||
var progress: Float = 0.0f,
|
||||
var error: Boolean = false,
|
||||
var finished: Boolean = false
|
||||
)
|
||||
|
||||
val EXAMPLE_MODELS = listOf(
|
||||
ModelInfo(
|
||||
name = "tiny-encoder-xatn.tflite",
|
||||
url = "example.com",
|
||||
size = 56L * 1024L * 1024L,
|
||||
progress = 0.5f,
|
||||
error = true
|
||||
),
|
||||
ModelInfo(
|
||||
name = "tiny-decoder.tflite",
|
||||
url = "example.com",
|
||||
size = 73L * 1024L * 1024L,
|
||||
progress = 0.3f,
|
||||
error = false
|
||||
),
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ModelItem(model: ModelInfo, showProgress: Boolean) {
|
||||
Column(modifier = Modifier.padding(4.dp)) {
|
||||
val color = if (model.error) {
|
||||
MaterialTheme.colorScheme.errorContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
}
|
||||
Surface(modifier = Modifier, color = color, shape = RoundedCornerShape(4.dp)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
if (model.error) {
|
||||
Icon(
|
||||
Icons.Default.Warning, contentDescription = "Failed", modifier = Modifier
|
||||
.align(CenterVertically)
|
||||
.padding(4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
val size = if (model.size != null) {
|
||||
"%.1f".format(model.size!!.toFloat() / 1000000.0f)
|
||||
} else {
|
||||
"?"
|
||||
}
|
||||
|
||||
Column {
|
||||
Text(model.name, style = Typography.bodyLarge)
|
||||
Text(
|
||||
"$size MB",
|
||||
style = Typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
if (showProgress && !model.error) {
|
||||
LinearProgressIndicator(
|
||||
progress = model.progress, modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(0.dp, 8.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun ScreenTitle(title: String, showBack: Boolean = false, navController: NavHostController = rememberNavController()) {
|
||||
val rowModifier = if(showBack) {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { navController.popBackStack() }
|
||||
} else {
|
||||
Modifier.fillMaxWidth()
|
||||
}
|
||||
Row(modifier = rowModifier) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
if(showBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back", modifier = Modifier.align(CenterVertically))
|
||||
Spacer(modifier = Modifier.width(18.dp))
|
||||
}
|
||||
Text(title, style = Typography.titleLarge, modifier = Modifier
|
||||
.align(CenterVertically)
|
||||
.padding(0.dp, 16.dp))
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun ScrollableList(content: @Composable () -> Unit) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun DownloadPrompt(
|
||||
onContinue: () -> Unit = {},
|
||||
onCancel: () -> Unit = {},
|
||||
models: List<ModelInfo> = EXAMPLE_MODELS
|
||||
) {
|
||||
ScrollableList {
|
||||
ScreenTitle(title = stringResource(R.string.download_required))
|
||||
Text(
|
||||
stringResource(R.string.download_required_body),
|
||||
style = Typography.bodyMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
for(model in models) {
|
||||
ModelItem(model, showProgress = false)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row {
|
||||
Button(
|
||||
onClick = onCancel, colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondary,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondary
|
||||
), modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.weight(1.0f)
|
||||
) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
Button(
|
||||
onClick = onContinue, modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.weight(1.5f)
|
||||
) {
|
||||
Text(stringResource(R.string.continue_))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun DownloadScreen(models: List<ModelInfo> = EXAMPLE_MODELS) {
|
||||
ScrollableList {
|
||||
ScreenTitle(stringResource(R.string.download_progress))
|
||||
if (models.any { it.error }) {
|
||||
Text(
|
||||
stringResource(R.string.download_failed),
|
||||
style = Typography.bodyMedium
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
stringResource(R.string.download_in_progress),
|
||||
style = Typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
for(model in models) {
|
||||
ModelItem(model, showProgress = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.fileNeedsDownloading(file: String): Boolean {
|
||||
return !File(this.filesDir, file).exists()
|
||||
}
|
||||
|
||||
class DownloadActivity : ComponentActivity() {
|
||||
private lateinit var modelsToDownload: List<ModelInfo>
|
||||
private val httpClient = OkHttpClient()
|
||||
private var isDownloading = false
|
||||
|
||||
private val themeOption: MutableState<ThemeOption?> = mutableStateOf(null)
|
||||
private fun updateContent() {
|
||||
setContent {
|
||||
themeOption.value?.let { themeOption ->
|
||||
val themeIdx = useDataStore(key = THEME_KEY.key, default = themeOption.key)
|
||||
val theme: ThemeOption = ThemeOptions[themeIdx.value] ?: themeOption
|
||||
UixThemeWrapper(theme.obtainColors(LocalContext.current)) {
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
if (isDownloading) {
|
||||
DownloadScreen(models = modelsToDownload)
|
||||
} else {
|
||||
DownloadPrompt(
|
||||
onContinue = { startDownload() },
|
||||
onCancel = { cancel() },
|
||||
models = modelsToDownload
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startDownload() {
|
||||
isDownloading = true
|
||||
updateContent()
|
||||
|
||||
modelsToDownload.forEach {
|
||||
val request = Request.Builder().method("GET", null).url(it.url).build()
|
||||
|
||||
httpClient.newCall(request).enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
it.error = true
|
||||
updateContent()
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
response.body?.source()?.let { source ->
|
||||
|
||||
try {
|
||||
it.size = response.headers["content-length"]!!.toLong()
|
||||
} catch (e: Exception) {
|
||||
println("url failed ${it.url}")
|
||||
println(response.headers)
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
val fileName = it.name + ".download"
|
||||
val file =
|
||||
File.createTempFile(fileName, null, this@DownloadActivity.cacheDir)
|
||||
val os = file.outputStream()
|
||||
|
||||
val buffer = ByteArray(128 * 1024)
|
||||
var downloaded = 0
|
||||
while (true) {
|
||||
val read = source.read(buffer)
|
||||
if (read == -1) {
|
||||
break
|
||||
}
|
||||
|
||||
os.write(buffer.sliceArray(0 until read))
|
||||
|
||||
downloaded += read
|
||||
|
||||
if (it.size != null) {
|
||||
it.progress = downloaded.toFloat() / it.size!!.toFloat()
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
updateContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it.finished = true
|
||||
it.progress = 1.0f
|
||||
os.flush()
|
||||
os.close()
|
||||
|
||||
assert(file.renameTo(File(this@DownloadActivity.filesDir, it.name)))
|
||||
|
||||
if (modelsToDownload.all { a -> a.finished }) {
|
||||
downloadsFinished()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancel() {
|
||||
val returnIntent = Intent()
|
||||
setResult(RESULT_CANCELED, returnIntent)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun downloadsFinished() {
|
||||
val returnIntent = Intent()
|
||||
setResult(RESULT_OK, returnIntent)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun obtainModelSizes() {
|
||||
modelsToDownload.forEach {
|
||||
val request =
|
||||
Request.Builder().method("HEAD", null).header("accept-encoding", "identity")
|
||||
.url(it.url).build()
|
||||
|
||||
httpClient.newCall(request).enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
it.error = true
|
||||
updateContent()
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
try {
|
||||
it.size = response.headers["content-length"]!!.toLong()
|
||||
} catch (e: Exception) {
|
||||
println("url failed ${it.url}")
|
||||
println(response.headers)
|
||||
e.printStackTrace()
|
||||
it.error = true
|
||||
}
|
||||
|
||||
if (response.code != 200) {
|
||||
println("Bad response code ${response.code}")
|
||||
it.error = true
|
||||
}
|
||||
updateContent()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val models = intent.getStringArrayListExtra("models")
|
||||
?: throw IllegalStateException("intent extra `models` must be specified for DownloadActivity")
|
||||
|
||||
modelsToDownload = models.distinct().filter { this.fileNeedsDownloading(it) }.map {
|
||||
ModelInfo(
|
||||
name = it,
|
||||
url = "https://voiceinput.futo.org/VoiceInput/${it}",
|
||||
size = null,
|
||||
progress = 0.0f
|
||||
)
|
||||
}
|
||||
|
||||
if (modelsToDownload.isEmpty()) {
|
||||
cancel()
|
||||
}
|
||||
|
||||
isDownloading = false
|
||||
|
||||
deferGetSetting(THEME_KEY) {
|
||||
val themeOptionFromSettings = ThemeOptions[it]
|
||||
val themeOption = when {
|
||||
themeOptionFromSettings == null -> VoiceInputTheme
|
||||
!themeOptionFromSettings.available(this) -> VoiceInputTheme
|
||||
else -> themeOptionFromSettings
|
||||
}
|
||||
|
||||
this.themeOption.value = themeOption
|
||||
}
|
||||
|
||||
updateContent()
|
||||
obtainModelSizes()
|
||||
}
|
||||
}
|