diff --git a/build.gradle b/build.gradle index a54bccff9..3924cae97 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/java/AndroidManifest.xml b/java/AndroidManifest.xml index 4bc131b9c..e25812552 100644 --- a/java/AndroidManifest.xml +++ b/java/AndroidManifest.xml @@ -52,7 +52,8 @@ android:protectionLevel="signature"/> - @@ -100,31 +102,20 @@ + + - - - - - - - - - - - - + + + diff --git a/java/res/drawable/ic_launcher_foreground.xml b/java/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..ff7b15db0 --- /dev/null +++ b/java/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + diff --git a/java/res/mipmap-anydpi-v26/ic_launcher.xml b/java/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..7353dbd1f --- /dev/null +++ b/java/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/java/res/mipmap-anydpi-v26/ic_launcher_round.xml b/java/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..7353dbd1f --- /dev/null +++ b/java/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/java/res/mipmap-hdpi/ic_launcher.png b/java/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..08f32206d Binary files /dev/null and b/java/res/mipmap-hdpi/ic_launcher.png differ diff --git a/java/res/mipmap-hdpi/ic_launcher_round.png b/java/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..7939c5c5b Binary files /dev/null and b/java/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/java/res/mipmap-mdpi/ic_launcher.png b/java/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..8d8cbadf7 Binary files /dev/null and b/java/res/mipmap-mdpi/ic_launcher.png differ diff --git a/java/res/mipmap-mdpi/ic_launcher_round.png b/java/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..a0275a731 Binary files /dev/null and b/java/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/java/res/mipmap-xhdpi/ic_launcher.png b/java/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..3f9fcc38a Binary files /dev/null and b/java/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/java/res/mipmap-xhdpi/ic_launcher_round.png b/java/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..d215dad55 Binary files /dev/null and b/java/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/java/res/mipmap-xxhdpi/ic_launcher.png b/java/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..3cc404504 Binary files /dev/null and b/java/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/java/res/mipmap-xxhdpi/ic_launcher_round.png b/java/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..e6e227e8b Binary files /dev/null and b/java/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/java/res/mipmap-xxxhdpi/ic_launcher.png b/java/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..1f6a82573 Binary files /dev/null and b/java/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/java/res/mipmap-xxxhdpi/ic_launcher_round.png b/java/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..c533c4fb9 Binary files /dev/null and b/java/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/java/res/values/ic_launcher_background.xml b/java/res/values/ic_launcher_background.xml new file mode 100644 index 000000000..97ff43571 --- /dev/null +++ b/java/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #1E293B + \ No newline at end of file diff --git a/java/res/values/strings-uix.xml b/java/res/values/strings-uix.xml index 9bf7e0b3b..579c94cef 100644 --- a/java/res/values/strings-uix.xml +++ b/java/res/values/strings-uix.xml @@ -5,9 +5,20 @@ AMOLED Dark Purple AOSP Material Dark - Voice Input Theme + AOSP Material Light + FUTO VI Theme Dynamic System Dynamic Light Dynamic Dark + + Voice Input Model Downloader + 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. + Continue + + Voice Input Model Download Progress + 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. + Downloading models… + + Model Downloader \ No newline at end of file diff --git a/java/res/xml/method.xml b/java/res/xml/method.xml index 151f87308..1f02f3b40 100644 --- a/java/res/xml/method.xml +++ b/java/res/xml/method.xml @@ -113,7 +113,7 @@ diff --git a/java/src/org/futo/inputmethod/latin/LatinIME.kt b/java/src/org/futo/inputmethod/latin/LatinIME.kt index 0441f34a8..6dfd0703d 100644 --- a/java/src/org/futo/inputmethod/latin/LatinIME.kt +++ b/java/src/org/futo/inputmethod/latin/LatinIME.kt @@ -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) diff --git a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java index 0800a4607..e54f01a57 100644 --- a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java +++ b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java @@ -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; } } diff --git a/java/src/org/futo/inputmethod/latin/SystemBroadcastReceiver.java b/java/src/org/futo/inputmethod/latin/SystemBroadcastReceiver.java index 0df761b65..3b5006ab3 100644 --- a/java/src/org/futo/inputmethod/latin/SystemBroadcastReceiver.java +++ b/java/src/org/futo/inputmethod/latin/SystemBroadcastReceiver.java @@ -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); + */ } } diff --git a/java/src/org/futo/inputmethod/latin/settings/SettingsActivity.java b/java/src/org/futo/inputmethod/latin/settings/SettingsActivity.java deleted file mode 100644 index afe437d2a..000000000 --- a/java/src/org/futo/inputmethod/latin/settings/SettingsActivity.java +++ /dev/null @@ -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); - } -} diff --git a/java/src/org/futo/inputmethod/latin/settings/SettingsFragment.java b/java/src/org/futo/inputmethod/latin/settings/SettingsFragment.java index b84df68f2..7e2f3950c 100644 --- a/java/src/org/futo/inputmethod/latin/settings/SettingsFragment.java +++ b/java/src/org/futo/inputmethod/latin/settings/SettingsFragment.java @@ -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; diff --git a/java/src/org/futo/inputmethod/latin/setup/SetupWizardActivity.java b/java/src/org/futo/inputmethod/latin/setup/SetupWizardActivity.java index 3cbb302e8..28c6df60a 100644 --- a/java/src/org/futo/inputmethod/latin/setup/SetupWizardActivity.java +++ b/java/src/org/futo/inputmethod/latin/setup/SetupWizardActivity.java @@ -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); } diff --git a/java/src/org/futo/inputmethod/latin/uix/BaseActions.kt b/java/src/org/futo/inputmethod/latin/uix/BaseActions.kt deleted file mode 100644 index e7cdaabbd..000000000 --- a/java/src/org/futo/inputmethod/latin/uix/BaseActions.kt +++ /dev/null @@ -1,5 +0,0 @@ -@file:Suppress("LocalVariableName") - -package org.futo.inputmethod.latin.uix - - diff --git a/java/src/org/futo/inputmethod/latin/uix/BasicThemeProvider.kt b/java/src/org/futo/inputmethod/latin/uix/BasicThemeProvider.kt index b1bc31de4..cc18d61d3 100644 --- a/java/src/org/futo/inputmethod/latin/uix/BasicThemeProvider.kt +++ b/java/src/org/futo/inputmethod/latin/uix/BasicThemeProvider.kt @@ -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 diff --git a/java/src/org/futo/inputmethod/latin/uix/Settings.kt b/java/src/org/futo/inputmethod/latin/uix/Settings.kt index a91cba25f..aeea79baa 100644 --- a/java/src/org/futo/inputmethod/latin/uix/Settings.kt +++ b/java/src/org/futo/inputmethod/latin/uix/Settings.kt @@ -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 by preferencesDataStore(name = "settings") @@ -28,6 +28,10 @@ suspend fun Context.getSetting(key: Preferences.Key, default: T): T { return valueFlow.first() } +fun Context.getSettingFlow(key: Preferences.Key, default: T): Flow { + return dataStore.data.map { preferences -> preferences[key] ?: default }.take(1) +} + suspend fun Context.setSetting(key: Preferences.Key, value: T) { this.dataStore.edit { preferences -> preferences[key] = value @@ -55,7 +59,10 @@ fun LifecycleOwner.deferGetSetting(key: Preferences.Key, 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( ) suspend fun Context.getSetting(key: SettingsKey): T { - val valueFlow: Flow = - this.dataStore.data.map { preferences -> preferences[key.key] ?: key.default }.take(1) + return getSetting(key.key, key.default) +} - return valueFlow.first() +fun Context.getSettingFlow(key: SettingsKey): Flow { + return getSettingFlow(key.key, key.default) } suspend fun Context.setSetting(key: SettingsKey, value: T) { - this.dataStore.edit { preferences -> - preferences[key.key] = value - } + return setSetting(key.key, value) } -val THEME_KEY = stringPreferencesKey("activeThemeOption") \ No newline at end of file +fun LifecycleOwner.deferGetSetting(key: SettingsKey, onObtained: (T) -> Unit): Job { + return deferGetSetting(key.key, key.default, onObtained) +} + +fun LifecycleOwner.deferSetSetting(key: SettingsKey, value: T): Job { + return deferSetSetting(key.key, value) +} + + +val THEME_KEY = SettingsKey( + key = stringPreferencesKey("activeThemeOption"), + default = DynamicSystemTheme.key +) \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/actions/ThemeAction.kt b/java/src/org/futo/inputmethod/latin/uix/actions/ThemeAction.kt index f0d1c3faf..63577e24a 100644 --- a/java/src/org/futo/inputmethod/latin/uix/actions/ThemeAction.kt +++ b/java/src/org/futo/inputmethod/latin/uix/actions/ThemeAction.kt @@ -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() { diff --git a/java/src/org/futo/inputmethod/latin/uix/actions/VoiceInputAction.kt b/java/src/org/futo/inputmethod/latin/uix/actions/VoiceInputAction.kt index 45ca031eb..d4ee4504f 100644 --- a/java/src/org/futo/inputmethod/latin/uix/actions/VoiceInputAction.kt +++ b/java/src/org/futo/inputmethod/latin/uix/actions/VoiceInputAction.kt @@ -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 = mutableStateOf(null) + private var modelException: MutableState = 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() } } diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/Components.kt b/java/src/org/futo/inputmethod/latin/uix/settings/Components.kt new file mode 100644 index 000000000..336cd6947 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/Components.kt @@ -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, + 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, + 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 -> {} + } + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/Hooks.kt b/java/src/org/futo/inputmethod/latin/uix/settings/Hooks.kt new file mode 100644 index 000000000..22799f06e --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/Hooks.kt @@ -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(val value: T, val setValue: (T) -> Job) +@Composable +fun useDataStore(key: Preferences.Key, default: T): DataStoreItem { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + val enableSoundFlow: Flow = 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 { + 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) +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/SettingsActivity.kt b/java/src/org/futo/inputmethod/latin/uix/settings/SettingsActivity.kt new file mode 100644 index 000000000..2154534ed --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/SettingsActivity.kt @@ -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 = 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() + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/SettingsNavigator.kt b/java/src/org/futo/inputmethod/latin/uix/settings/SettingsNavigator.kt new file mode 100644 index 000000000..982d7b188 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/SettingsNavigator.kt @@ -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) } + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/SettingsUtils.kt b/java/src/org/futo/inputmethod/latin/uix/settings/SettingsUtils.kt new file mode 100644 index 000000000..d15ec490c --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/SettingsUtils.kt @@ -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) +} diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/Setup.kt b/java/src/org/futo/inputmethod/latin/uix/settings/Setup.kt new file mode 100644 index 000000000..620d6476d --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/Setup.kt @@ -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") + } + } + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Home.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Home.kt new file mode 100644 index 000000000..d1482878c --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Home.kt @@ -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) + ) + */ + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/PredictiveText.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/PredictiveText.kt new file mode 100644 index 000000000..767721a46 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/PredictiveText.kt @@ -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) + ) + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/ThemeScreen.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/ThemeScreen.kt new file mode 100644 index 000000000..0e2514377 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/ThemeScreen.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Typing.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Typing.kt new file mode 100644 index 000000000..78d0e1b08 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Typing.kt @@ -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 + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt new file mode 100644 index 000000000..bcd0f7421 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt @@ -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, setting: SettingsKey) { + 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 + ) + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/theme/Theme.kt b/java/src/org/futo/inputmethod/latin/uix/theme/Theme.kt index 4c36da67b..d2f61f420 100644 --- a/java/src/org/futo/inputmethod/latin/uix/theme/Theme.kt +++ b/java/src/org/futo/inputmethod/latin/uix/theme/Theme.kt @@ -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( diff --git a/java/src/org/futo/inputmethod/latin/uix/theme/ThemeOptions.kt b/java/src/org/futo/inputmethod/latin/uix/theme/ThemeOptions.kt index 29f472304..7807b9b53 100644 --- a/java/src/org/futo/inputmethod/latin/uix/theme/ThemeOptions.kt +++ b/java/src/org/futo/inputmethod/latin/uix/theme/ThemeOptions.kt @@ -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, ) \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/theme/presets/AMOLEDDarkPurple.kt b/java/src/org/futo/inputmethod/latin/uix/theme/presets/AMOLEDDarkPurple.kt index e25366345..30a7cf10f 100644 --- a/java/src/org/futo/inputmethod/latin/uix/theme/presets/AMOLEDDarkPurple.kt +++ b/java/src/org/futo/inputmethod/latin/uix/theme/presets/AMOLEDDarkPurple.kt @@ -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) } \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/theme/presets/ClassicMaterialDark.kt b/java/src/org/futo/inputmethod/latin/uix/theme/presets/ClassicMaterialDark.kt index 1d58b07e6..0a9306974 100644 --- a/java/src/org/futo/inputmethod/latin/uix/theme/presets/ClassicMaterialDark.kt +++ b/java/src/org/futo/inputmethod/latin/uix/theme/presets/ClassicMaterialDark.kt @@ -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) } \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/theme/presets/ClassicMaterialLight.kt b/java/src/org/futo/inputmethod/latin/uix/theme/presets/ClassicMaterialLight.kt new file mode 100644 index 000000000..2ff6bf798 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/theme/presets/ClassicMaterialLight.kt @@ -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) +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/theme/presets/VoiceInputTheme.kt b/java/src/org/futo/inputmethod/latin/uix/theme/presets/VoiceInputTheme.kt index 2a50805c6..f772abb7f 100644 --- a/java/src/org/futo/inputmethod/latin/uix/theme/presets/VoiceInputTheme.kt +++ b/java/src/org/futo/inputmethod/latin/uix/theme/presets/VoiceInputTheme.kt @@ -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) } \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/theme/selector/ThemePicker.kt b/java/src/org/futo/inputmethod/latin/uix/theme/selector/ThemePicker.kt new file mode 100644 index 000000000..acbaea11a --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/theme/selector/ThemePicker.kt @@ -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 {} + } + } + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/voiceinput/downloader/DownloadActivity.kt b/java/src/org/futo/inputmethod/latin/uix/voiceinput/downloader/DownloadActivity.kt new file mode 100644 index 000000000..f69408ca7 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/voiceinput/downloader/DownloadActivity.kt @@ -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 = 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 = 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 + private val httpClient = OkHttpClient() + private var isDownloading = false + + private val themeOption: MutableState = 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() + } +}