Merge branch 'new-settings-menu' into 'master'

New settings menu

See merge request alex/latinime!2
This commit is contained in:
Aleksandras Kostarevas 2023-09-04 17:10:07 +00:00
commit f1ff995cc7
50 changed files with 2150 additions and 184 deletions

View File

@ -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'

View File

@ -52,7 +52,8 @@
android:protectionLevel="signature"/>
<application android:label="@string/english_ime_name"
android:icon="@drawable/ic_launcher_keyboard"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:allowBackup="true"
android:defaultToDeviceProtectedStorage="true"
@ -87,12 +88,13 @@
</service>
<!-- Activities -->
<activity android:name=".setup.SetupActivity"
android:theme="@style/platformActivityTheme"
<activity android:name=".uix.settings.SettingsActivity"
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"
android:label="@string/english_ime_name"
android:icon="@drawable/ic_launcher_keyboard"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:launchMode="singleTask"
android:noHistory="true"
android:noHistory="false"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@ -100,31 +102,20 @@
</intent-filter>
</activity>
<activity
android:name=".uix.voiceinput.downloader.DownloadActivity"
android:exported="false"
android:label="@string/model_downloader"
android:clearTaskOnLaunch="true"
android:launchMode="singleTask"
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar" />
<activity android:name=".permissions.PermissionsActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:exported="false"
android:taskAffinity="">
</activity>
<activity android:name=".setup.SetupWizardActivity"
android:theme="@style/platformActivityTheme"
android:label="@string/english_ime_name"
android:clearTaskOnLaunch="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
</activity>
<activity android:name=".settings.SettingsActivity"
android:theme="@style/platformSettingsTheme"
android:label="@string/english_ime_settings"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
</activity>
<activity android:name=".spellcheck.SpellCheckerSettingsActivity"
android:theme="@style/platformSettingsTheme"
android:label="@string/android_spell_checker_settings"

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="92dp"
android:height="24dp"
android:viewportWidth="92"
android:viewportHeight="24">
<path
android:pathData="M91.636,12C91.636,18.627 86.267,24 79.644,24C73.02,24 67.651,18.627 67.651,12C67.651,5.373 73.02,0 79.644,0C86.267,0 91.636,5.373 91.636,12ZM76.15,14.422C74.92,13.191 74.305,12.575 74.305,11.811C74.305,11.046 74.92,10.431 76.15,9.2L77.153,8.197C78.383,6.966 78.998,6.351 79.762,6.351C80.526,6.351 81.141,6.966 82.371,8.197L83.374,9.2C84.604,10.431 85.219,11.046 85.219,11.811C85.219,12.575 84.604,13.191 83.374,14.422L82.371,15.425C81.141,16.655 80.526,17.271 79.762,17.271C78.998,17.271 78.383,16.655 77.153,15.425L76.15,14.422ZM16.913,7.077C17.252,7.077 17.528,6.801 17.528,6.462V1.846C17.528,1.506 17.252,1.231 16.913,1.231H0.615C0.275,1.231 0,1.506 0,1.846V22.154C0,22.494 0.275,22.769 0.615,22.769H6.15C6.49,22.769 6.765,22.494 6.765,22.154V16.492C6.765,16.152 7.04,15.877 7.38,15.877H14.822C15.161,15.877 15.437,15.601 15.437,15.262V10.646C15.437,10.306 15.161,10.031 14.822,10.031H7.38C7.04,10.031 6.765,9.755 6.765,9.415V7.692C6.765,7.352 7.04,7.077 7.38,7.077H16.913ZM31.209,23.139H31.302C37.882,23.139 41.91,19.631 41.91,12.954V1.846C41.91,1.506 41.635,1.231 41.295,1.231H35.76C35.421,1.231 35.145,1.506 35.145,1.846V12.339C35.145,14.615 34.161,16.615 31.302,16.615H31.209C28.38,16.615 27.365,14.615 27.365,12.339V1.846C27.365,1.506 27.09,1.231 26.75,1.231H21.215C20.876,1.231 20.6,1.506 20.6,1.846V12.954C20.6,19.631 24.629,23.139 31.209,23.139ZM44.985,1.846C44.985,1.506 45.26,1.231 45.599,1.231H65.464C65.804,1.231 66.079,1.506 66.079,1.846V6.554C66.079,6.894 65.804,7.169 65.464,7.169H59.529C59.19,7.169 58.915,7.445 58.915,7.785V22.154C58.915,22.494 58.639,22.769 58.299,22.769H52.764C52.425,22.769 52.149,22.494 52.149,22.154V7.785C52.149,7.445 51.874,7.169 51.534,7.169H45.599C45.26,7.169 44.985,6.894 44.985,6.554V1.846Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,52 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="128"
android:viewportHeight="128">
<group android:scaleX="0.63"
android:scaleY="0.63"
android:translateX="23.68"
android:translateY="23.68">
<group>
<clip-path
android:pathData="M0,0h128v128h-128z"/>
<path
android:pathData="M114.5,49.5C114.5,56.96 108.58,63 101.28,63C93.98,63 88.06,56.96 88.06,49.5C88.06,42.04 93.98,36 101.28,36C108.58,36 114.5,42.04 114.5,49.5ZZM97.43,52.22C96.08,50.84 95.4,50.15 95.4,49.29C95.4,48.43 96.08,47.73 97.43,46.35L98.54,45.22C99.89,43.84 100.57,43.14 101.41,43.14C102.25,43.14 102.93,43.84 104.29,45.22L105.39,46.35C106.75,47.73 107.43,48.43 107.43,49.29C107.43,50.15 106.75,50.84 105.39,52.22L104.29,53.35C102.93,54.74 102.25,55.43 101.41,55.43C100.57,55.43 99.89,54.74 98.54,53.35L97.43,52.22ZZM32.14,43.96C32.51,43.96 32.82,43.65 32.82,43.27L32.82,38.08C32.82,37.69 32.51,37.38 32.14,37.38L14.18,37.38C13.8,37.38 13.5,37.69 13.5,38.08L13.5,60.92C13.5,61.31 13.8,61.62 14.18,61.62L20.28,61.62C20.65,61.62 20.96,61.31 20.96,60.92L20.96,54.55C20.96,54.17 21.26,53.86 21.63,53.86L29.84,53.86C30.21,53.86 30.51,53.55 30.51,53.17L30.51,47.98C30.51,47.6 30.21,47.28 29.84,47.28L21.63,47.28C21.26,47.28 20.96,46.97 20.96,46.59L20.96,44.65C20.96,44.27 21.26,43.96 21.63,43.96L32.14,43.96ZZM47.9,62.03L48,62.03C55.25,62.03 59.69,58.08 59.69,50.57L59.69,38.08C59.69,37.69 59.39,37.38 59.01,37.38L52.91,37.38C52.54,37.38 52.24,37.69 52.24,38.08L52.24,49.88C52.24,52.44 51.15,54.69 48,54.69L47.9,54.69C44.78,54.69 43.66,52.44 43.66,49.88L43.66,38.08C43.66,37.69 43.36,37.38 42.98,37.38L36.88,37.38C36.51,37.38 36.21,37.69 36.21,38.08L36.21,50.57C36.21,58.08 40.65,62.03 47.9,62.03ZZM63.08,38.08C63.08,37.69 63.38,37.38 63.76,37.38L85.65,37.38C86.03,37.38 86.33,37.69 86.33,38.08L86.33,43.37C86.33,43.76 86.03,44.06 85.65,44.06L79.11,44.06C78.74,44.06 78.43,44.38 78.43,44.76L78.43,60.92C78.43,61.31 78.13,61.62 77.76,61.62L71.66,61.62C71.28,61.62 70.98,61.31 70.98,60.92L70.98,44.76C70.98,44.38 70.68,44.06 70.3,44.06L63.76,44.06C63.38,44.06 63.08,43.76 63.08,43.37L63.08,38.08ZZ"
android:fillColor="#FFFFFF"
android:fillType="evenOdd"/>
<path
android:pathData="M35,72L40.54,72A2,2 0,0 1,42.54 74L42.54,79.6A2,2 0,0 1,40.54 81.6L35,81.6A2,2 0,0 1,33 79.6L33,74A2,2 0,0 1,35 72z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M35,85.2L40.54,85.2A2,2 0,0 1,42.54 87.2L42.54,92.8A2,2 0,0 1,40.54 94.8L35,94.8A2,2 0,0 1,33 92.8L33,87.2A2,2 0,0 1,35 85.2z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M48.12,72L53.65,72A2,2 0,0 1,55.65 74L55.65,79.6A2,2 0,0 1,53.65 81.6L48.12,81.6A2,2 0,0 1,46.12 79.6L46.12,74A2,2 0,0 1,48.12 72z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M48.12,85.2L53.65,85.2A2,2 0,0 1,55.65 87.2L55.65,92.8A2,2 0,0 1,53.65 94.8L48.12,94.8A2,2 0,0 1,46.12 92.8L46.12,87.2A2,2 0,0 1,48.12 85.2z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M61.23,72L66.77,72A2,2 0,0 1,68.77 74L68.77,79.6A2,2 0,0 1,66.77 81.6L61.23,81.6A2,2 0,0 1,59.23 79.6L59.23,74A2,2 0,0 1,61.23 72z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M61.23,85.2L66.77,85.2A2,2 0,0 1,68.77 87.2L68.77,92.8A2,2 0,0 1,66.77 94.8L61.23,94.8A2,2 0,0 1,59.23 92.8L59.23,87.2A2,2 0,0 1,61.23 85.2z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M45.73,98.4L82.27,98.4A2,2 0,0 1,84.27 100.4L84.27,106A2,2 0,0 1,82.27 108L45.73,108A2,2 0,0 1,43.73 106L43.73,100.4A2,2 0,0 1,45.73 98.4z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M74.35,72L79.88,72A2,2 0,0 1,81.88 74L81.88,79.6A2,2 0,0 1,79.88 81.6L74.35,81.6A2,2 0,0 1,72.35 79.6L72.35,74A2,2 0,0 1,74.35 72z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M74.35,85.2L79.88,85.2A2,2 0,0 1,81.88 87.2L81.88,92.8A2,2 0,0 1,79.88 94.8L74.35,94.8A2,2 0,0 1,72.35 92.8L72.35,87.2A2,2 0,0 1,74.35 85.2z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M87.46,72L93,72A2,2 0,0 1,95 74L95,79.6A2,2 0,0 1,93 81.6L87.46,81.6A2,2 0,0 1,85.46 79.6L85.46,74A2,2 0,0 1,87.46 72z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M87.46,85.2L93,85.2A2,2 0,0 1,95 87.2L95,92.8A2,2 0,0 1,93 94.8L87.46,94.8A2,2 0,0 1,85.46 92.8L85.46,87.2A2,2 0,0 1,87.46 85.2z"
android:fillColor="#FFFFFF"/>
</group>
</group>
</vector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#1E293B</color>
</resources>

View File

@ -5,9 +5,20 @@
<string name="amoled_dark_theme_name">AMOLED Dark Purple</string>
<string name="classic_material_dark_theme_name">AOSP Material Dark</string>
<string name="voice_input_theme_name">Voice Input Theme</string>
<string name="classic_material_light_theme_name">AOSP Material Light</string>
<string name="voice_input_theme_name">FUTO VI Theme</string>
<string name="dynamic_system_theme_name">Dynamic System</string>
<string name="dynamic_light_theme_name">Dynamic Light</string>
<string name="dynamic_dark_theme_name">Dynamic Dark</string>
<string name="download_required">Voice Input Model Downloader</string>
<string name="download_required_body">Download of some model files is necessary for voice input functionality. This may incur data fees if you are using Mobile Data instead of Wi-Fi.</string>
<string name="continue_">Continue</string>
<string name="download_progress">Voice Input Model Download Progress</string>
<string name="download_failed">Download of one or more resources has failed. Please make sure you\'re connected to a network, the app has network permission, or try again later.</string>
<string name="download_in_progress">Downloading models…</string>
<string name="model_downloader">Model Downloader</string>
</resources>

View File

@ -113,7 +113,7 @@
<!-- If IME doesn't have an applicable subtype, the first subtype will be used as a default
subtype.-->
<input-method xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsActivity="org.futo.inputmethod.latin.settings.SettingsActivity"
android:settingsActivity="org.futo.inputmethod.latin.uix.settings.SettingsActivity"
android:isDefault="@bool/im_is_default"
android:supportsSwitchingToNextInputMethod="true"
android:supportsInlineSuggestions="true">

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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);
*/
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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);
}

View File

@ -1,5 +0,0 @@
@file:Suppress("LocalVariableName")
package org.futo.inputmethod.latin.uix

View File

@ -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

View File

@ -6,7 +6,6 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
@ -18,6 +17,7 @@ import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.futo.inputmethod.latin.uix.theme.presets.DynamicSystemTheme
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
@ -28,6 +28,10 @@ suspend fun <T> Context.getSetting(key: Preferences.Key<T>, default: T): T {
return valueFlow.first()
}
fun <T> Context.getSettingFlow(key: Preferences.Key<T>, default: T): Flow<T> {
return dataStore.data.map { preferences -> preferences[key] ?: default }.take(1)
}
suspend fun <T> Context.setSetting(key: Preferences.Key<T>, value: T) {
this.dataStore.edit { preferences ->
preferences[key] = value
@ -55,7 +59,10 @@ fun <T> LifecycleOwner.deferGetSetting(key: Preferences.Key<T>, default: T, onOb
return lifecycleScope.launch {
withContext(Dispatchers.Default) {
val value = context.getSetting(key, default)
onObtained(value)
withContext(Dispatchers.Main) {
onObtained(value)
}
}
}
}
@ -75,16 +82,27 @@ data class SettingsKey<T>(
)
suspend fun <T> Context.getSetting(key: SettingsKey<T>): T {
val valueFlow: Flow<T> =
this.dataStore.data.map { preferences -> preferences[key.key] ?: key.default }.take(1)
return getSetting(key.key, key.default)
}
return valueFlow.first()
fun <T> Context.getSettingFlow(key: SettingsKey<T>): Flow<T> {
return getSettingFlow(key.key, key.default)
}
suspend fun <T> Context.setSetting(key: SettingsKey<T>, value: T) {
this.dataStore.edit { preferences ->
preferences[key.key] = value
}
return setSetting(key.key, value)
}
val THEME_KEY = stringPreferencesKey("activeThemeOption")
fun <T> LifecycleOwner.deferGetSetting(key: SettingsKey<T>, onObtained: (T) -> Unit): Job {
return deferGetSetting(key.key, key.default, onObtained)
}
fun <T> LifecycleOwner.deferSetSetting(key: SettingsKey<T>, value: T): Job {
return deferSetSetting(key.key, value)
}
val THEME_KEY = SettingsKey(
key = stringPreferencesKey("activeThemeOption"),
default = DynamicSystemTheme.key
)

View File

@ -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() {

View File

@ -1,15 +1,20 @@
package org.futo.inputmethod.latin.uix.actions
import android.app.Activity
import android.content.Intent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@ -32,6 +37,7 @@ import org.futo.inputmethod.latin.uix.MULTILINGUAL_MODEL_INDEX
import org.futo.inputmethod.latin.uix.PersistentActionState
import org.futo.inputmethod.latin.uix.VERBOSE_PROGRESS
import org.futo.inputmethod.latin.uix.getSetting
import org.futo.inputmethod.latin.uix.voiceinput.downloader.DownloadActivity
import org.futo.voiceinput.shared.ENGLISH_MODELS
import org.futo.voiceinput.shared.MULTILINGUAL_MODELS
import org.futo.voiceinput.shared.ModelDoesNotExistException
@ -111,6 +117,7 @@ private class VoiceInputActionWindow(
}
private var recognizerView: MutableState<RecognizerView?> = mutableStateOf(null)
private var modelException: MutableState<ModelDoesNotExistException?> = mutableStateOf(null)
private val initJob = manager.getLifecycleScope().launch {
yield()
@ -128,8 +135,7 @@ private class VoiceInputActionWindow(
modelManager = state.modelManager
)
} catch(e: ModelDoesNotExistException) {
// TODO: Show an error to the user, with an option to download
close()
modelException.value = e
return@launch
}
@ -151,6 +157,23 @@ private class VoiceInputActionWindow(
return inputTransaction!!
}
@Composable
private fun ModelDownloader(modelException: ModelDoesNotExistException) {
val context = LocalContext.current
Box(modifier = Modifier.fillMaxSize().clickable {
val intent = Intent(context, DownloadActivity::class.java)
intent.putStringArrayListExtra("models", ArrayList(modelException.models.map { model -> model.getRequiredDownloadList(context) }.flatten()))
if(context !is Activity) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
}) {
Text("Tap to complete setup", modifier = Modifier.align(Alignment.Center))
}
}
@Composable
override fun windowName(): String {
return stringResource(R.string.voice_input_action_title)
@ -167,7 +190,10 @@ private class VoiceInputActionWindow(
indication = null,
interactionSource = remember { MutableInteractionSource() })) {
Box(modifier = Modifier.align(Alignment.Center)) {
recognizerView.value?.Content()
when {
modelException.value != null -> ModelDownloader(modelException.value!!)
recognizerView.value != null -> recognizerView.value!!.Content()
}
}
}
}
@ -178,12 +204,14 @@ private class VoiceInputActionWindow(
}
private var wasFinished = false
private var cancelPlayed = false
override fun cancelled() {
if (!wasFinished) {
if (shouldPlaySounds) {
if (shouldPlaySounds && !cancelPlayed) {
state.soundPlayer.playCancelSound()
cancelPlayed = true
}
getOrStartInputTransaction().cancel()
inputTransaction?.cancel()
}
}

View File

@ -0,0 +1,295 @@
package org.futo.inputmethod.latin.uix.settings
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import org.futo.inputmethod.latin.uix.SettingsKey
import org.futo.inputmethod.latin.uix.theme.Typography
@Composable
fun ScreenTitle(title: String, showBack: Boolean = false, navController: NavHostController = rememberNavController()) {
val rowModifier = if(showBack) {
Modifier
.fillMaxWidth()
.clickable { navController.popBackStack() }
} else {
Modifier.fillMaxWidth()
}
Row(modifier = rowModifier) {
Spacer(modifier = Modifier.width(16.dp))
if(showBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back", modifier = Modifier.align(CenterVertically))
Spacer(modifier = Modifier.width(18.dp))
}
Text(title, style = Typography.titleLarge, modifier = Modifier
.align(CenterVertically)
.padding(0.dp, 16.dp))
}
}
@Composable
@Preview
fun Tip(text: String = "This is an example tip") {
Surface(
color = MaterialTheme.colorScheme.primaryContainer, modifier = Modifier
.fillMaxWidth()
.padding(8.dp), shape = RoundedCornerShape(4.dp)
) {
Text(
text,
modifier = Modifier.padding(8.dp),
style = Typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
@Composable
fun SettingItem(
title: String,
subtitle: String? = null,
onClick: () -> Unit,
icon: (@Composable () -> Unit)? = null,
disabled: Boolean = false,
content: @Composable () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.defaultMinSize(0.dp, 68.dp)
.clickable(enabled = !disabled, onClick = {
if (!disabled) {
onClick()
}
})
.padding(0.dp, 4.dp, 0.dp, 4.dp)
) {
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier
.width(48.dp)
.align(Alignment.CenterVertically)
) {
Box(modifier = Modifier.align(Alignment.CenterHorizontally)) {
if (icon != null) {
icon()
}
}
}
Spacer(modifier = Modifier.width(12.dp))
Row(
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically)
.alpha(
if (disabled) {
0.5f
} else {
1.0f
}
)
) {
Column {
Text(title, style = Typography.bodyLarge)
if (subtitle != null) {
Text(
subtitle,
style = Typography.bodySmall,
color = MaterialTheme.colorScheme.outline
)
}
}
}
Box(modifier = Modifier.align(Alignment.CenterVertically)) {
content()
}
Spacer(modifier = Modifier.width(12.dp))
}
}
@Composable
fun SettingToggleRaw(
title: String,
enabled: Boolean,
setValue: (Boolean) -> Unit,
subtitle: String? = null,
disabled: Boolean = false,
icon: (@Composable () -> Unit)? = null
) {
SettingItem(
title = title,
subtitle = subtitle,
onClick = {
if (!disabled) {
setValue(!enabled)
}
},
icon = icon
) {
Switch(checked = enabled, onCheckedChange = {
if (!disabled) {
setValue(!enabled)
}
}, enabled = !disabled)
}
}
@Composable
fun SettingToggleDataStoreItem(
title: String,
dataStoreItem: DataStoreItem<Boolean>,
subtitle: String? = null,
disabledSubtitle: String? = null,
disabled: Boolean = false,
icon: (@Composable () -> Unit)? = null
) {
val (enabled, setValue) = dataStoreItem
val subtitleValue = if (!enabled && disabledSubtitle != null) {
disabledSubtitle
} else {
subtitle
}
SettingToggleRaw(title, enabled, { setValue(it) }, subtitleValue, disabled, icon)
}
@Composable
fun SettingToggleDataStore(
title: String,
setting: SettingsKey<Boolean>,
subtitle: String? = null,
disabledSubtitle: String? = null,
disabled: Boolean = false,
icon: (@Composable () -> Unit)? = null
) {
SettingToggleDataStoreItem(
title, useDataStore(setting.key, setting.default), subtitle, disabledSubtitle, disabled, icon)
}
@Composable
fun SettingToggleSharedPrefs(
title: String,
key: String,
default: Boolean,
subtitle: String? = null,
disabledSubtitle: String? = null,
disabled: Boolean = false,
icon: (@Composable () -> Unit)? = null
) {
SettingToggleDataStoreItem(
title, useSharedPrefsBool(key, default), subtitle, disabledSubtitle, disabled, icon)
}
@Composable
fun ScrollableList(content: @Composable () -> Unit) {
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
) {
content()
}
}
@Composable
fun SettingListLazy(content: LazyListScope.() -> Unit) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
) {
content()
}
}
enum class NavigationItemStyle {
HomePrimary,
HomeSecondary,
HomeTertiary,
Misc
}
@Composable
fun NavigationItem(title: String, style: NavigationItemStyle, navigate: () -> Unit, icon: Painter? = null) {
SettingItem(
title = title,
onClick = navigate,
icon = {
icon?.let {
val circleColor = when(style) {
NavigationItemStyle.HomePrimary -> MaterialTheme.colorScheme.primaryContainer
NavigationItemStyle.HomeSecondary -> MaterialTheme.colorScheme.secondaryContainer
NavigationItemStyle.HomeTertiary -> MaterialTheme.colorScheme.tertiaryContainer
NavigationItemStyle.Misc -> Color.Transparent
}
val iconColor = when(style) {
NavigationItemStyle.HomePrimary -> MaterialTheme.colorScheme.onPrimaryContainer
NavigationItemStyle.HomeSecondary -> MaterialTheme.colorScheme.onSecondaryContainer
NavigationItemStyle.HomeTertiary -> MaterialTheme.colorScheme.onTertiaryContainer
NavigationItemStyle.Misc -> MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f)
}
Canvas(modifier = Modifier.fillMaxSize()) {
drawCircle(circleColor, this.size.maxDimension / 2.4f)
translate(
left = this.size.width / 2.0f - icon.intrinsicSize.width / 2.0f,
top = this.size.height / 2.0f - icon.intrinsicSize.height / 2.0f
) {
with(icon) {
draw(icon.intrinsicSize, colorFilter = ColorFilter.tint(iconColor))
}
}
}
}
}
) {
when(style) {
NavigationItemStyle.Misc -> Icon(Icons.Default.ArrowForward, contentDescription = "Go")
else -> {}
}
}
}

View File

@ -0,0 +1,88 @@
package org.futo.inputmethod.latin.uix.settings
import android.content.SharedPreferences
import android.preference.PreferenceManager
import android.provider.Settings
import android.view.inputmethod.InputMethodManager
import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.edit
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.futo.inputmethod.latin.uix.dataStore
data class DataStoreItem<T>(val value: T, val setValue: (T) -> Job)
@Composable
fun <T> useDataStore(key: Preferences.Key<T>, default: T): DataStoreItem<T> {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val enableSoundFlow: Flow<T> = remember {
context.dataStore.data.map {
preferences -> preferences[key] ?: default
}
}
val value = enableSoundFlow.collectAsState(initial = default).value!!
val setValue = { newValue: T ->
coroutineScope.launch {
context.dataStore.edit { preferences ->
preferences[key] = newValue
}
}
}
return DataStoreItem(value, setValue)
}
@Composable
fun useSharedPrefsBool(key: String, default: Boolean): DataStoreItem<Boolean> {
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val sharedPrefs = remember { PreferenceManager.getDefaultSharedPreferences(context) }
val value = remember { mutableStateOf(sharedPrefs.getBoolean(key, default)) }
// This is not the most efficient way to do this... but it works for a settings menu
DisposableEffect(Unit) {
val listener =
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, changedKey ->
if (key == changedKey) {
value.value = sharedPreferences.getBoolean(key, value.value)
}
}
sharedPrefs.registerOnSharedPreferenceChangeListener(listener)
onDispose {
sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener)
}
}
val setValue = { newValue: Boolean ->
coroutineScope.launch {
withContext(Dispatchers.Main) {
sharedPrefs.edit {
putBoolean(key, newValue)
}
}
}
}
return DataStoreItem(value.value, setValue)
}

View File

@ -0,0 +1,160 @@
package org.futo.inputmethod.latin.uix.settings
import android.content.Context
import android.content.Context.INPUT_METHOD_SERVICE
import android.content.Intent
import android.os.Bundle
import android.provider.Settings
import android.view.inputmethod.InputMethodManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.futo.inputmethod.latin.uix.THEME_KEY
import org.futo.inputmethod.latin.uix.deferGetSetting
import org.futo.inputmethod.latin.uix.theme.StatusBarColorSetter
import org.futo.inputmethod.latin.uix.theme.ThemeOption
import org.futo.inputmethod.latin.uix.theme.ThemeOptions
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme
private fun Context.isInputMethodEnabled(): Boolean {
val packageName = packageName
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
var found = false
for (imi in imm.enabledInputMethodList) {
if (packageName == imi.packageName) {
found = true
}
}
return found
}
private fun Context.isDefaultIMECurrent(): Boolean {
val value = Settings.Secure.getString(contentResolver, Settings.Secure.DEFAULT_INPUT_METHOD)
return value.startsWith(packageName)
}
class SettingsActivity : ComponentActivity() {
private val themeOption: MutableState<ThemeOption?> = mutableStateOf(null)
private val inputMethodEnabled = mutableStateOf(false)
private val inputMethodSelected = mutableStateOf(false)
private var wasImeEverDisabled = false
companion object {
private var pollJob: Job? = null
}
@OptIn(DelicateCoroutinesApi::class)
private fun updateSystemState() {
val inputMethodEnabled = isInputMethodEnabled()
val inputMethodSelected = isDefaultIMECurrent()
this.inputMethodEnabled.value = inputMethodEnabled
this.inputMethodSelected.value = inputMethodSelected
if(!inputMethodEnabled) {
wasImeEverDisabled = true
} else if(wasImeEverDisabled) {
// We just went from inputMethodEnabled==false to inputMethodEnabled==true
// This is because the user is in the input method settings screen and just turned on
// our IME. We can bring them back here so that they don't have to press back button
wasImeEverDisabled = false
val intent = Intent()
intent.setClass(this, SettingsActivity::class.java)
intent.flags = (Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
or Intent.FLAG_ACTIVITY_SINGLE_TOP
or Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)
}
if(!inputMethodEnabled || !inputMethodSelected) {
if(pollJob == null || !pollJob!!.isActive) {
pollJob = GlobalScope.launch {
systemStatePoller()
}
}
}
}
private suspend fun systemStatePoller() {
while(!this.inputMethodEnabled.value || !this.inputMethodSelected.value) {
delay(200)
updateSystemState()
}
}
private fun updateContent() {
setContent {
themeOption.value?.let { themeOption ->
val themeIdx = useDataStore(key = THEME_KEY.key, default = themeOption.key)
val theme: ThemeOption = ThemeOptions[themeIdx.value] ?: themeOption
UixThemeWrapper(theme.obtainColors(LocalContext.current)) {
StatusBarColorSetter()
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
SetupOrMain(inputMethodEnabled.value, inputMethodSelected.value) {
SettingsNavigator()
}
}
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
updateSystemState()
updateContent()
}
}
deferGetSetting(THEME_KEY) {
val themeOptionFromSettings = ThemeOptions[it]
val themeOption = when {
themeOptionFromSettings == null -> VoiceInputTheme
!themeOptionFromSettings.available(this) -> VoiceInputTheme
else -> themeOptionFromSettings
}
this.themeOption.value = themeOption
}
}
override fun onResume() {
super.onResume()
updateSystemState()
}
override fun onRestart() {
super.onRestart()
updateSystemState()
}
}

View File

@ -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) }
}
}

View File

@ -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)
}

View File

@ -0,0 +1,147 @@
package org.futo.inputmethod.latin.uix.settings
import android.content.Context
import android.content.Intent
import android.provider.Settings
import android.view.inputmethod.InputMethodManager
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.uix.theme.Typography
@Composable
fun SetupContainer(inner: @Composable () -> Unit) {
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxWidth(fraction = 1.0f)
.fillMaxHeight(fraction = 0.4f)
) {
Icon(
painter = painterResource(id = R.drawable.futo_logo),
contentDescription = "FUTO Logo",
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(0.75f)
.align(Alignment.CenterHorizontally),
tint = MaterialTheme.colorScheme.onBackground
)
}
Row(
modifier = Modifier.fillMaxSize()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(fraction = 0.5f)
.align(Alignment.CenterVertically)
.padding(32.dp)
) {
Box(modifier = Modifier.align(Alignment.CenterVertically)) {
inner()
}
}
}
}
}
@Composable
fun Step(fraction: Float, text: String) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text, style = Typography.labelSmall)
LinearProgressIndicator(progress = fraction, modifier = Modifier.fillMaxWidth())
}
}
// TODO: May wish to have a skip option
@Composable
@Preview
fun SetupEnableIME() {
val context = LocalContext.current
val launchImeOptions = {
// TODO: look into direct boot to get rid of direct boot warning?
val intent = Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)
intent.flags = (Intent.FLAG_ACTIVITY_NEW_TASK
or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
or Intent.FLAG_ACTIVITY_NO_HISTORY
or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
context.startActivity(intent)
}
SetupContainer {
Column {
Step(fraction = 1.0f/3.0f, text = "Setup - Step 1 of 2")
Text(
"To use FUTO Keyboard, you must first enable FUTO Keyboard as an input method.",
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = launchImeOptions,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text("Open Input Method Settings")
}
}
}
}
@Composable
@Preview
fun SetupChangeDefaultIME() {
val context = LocalContext.current
val launchImeOptions = {
val inputMethodManager =
context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.showInputMethodPicker()
}
SetupContainer {
Column {
Step(fraction = 2.0f/3.0f, text = "Setup - Step 2 of 2")
Text(
"Next, select FUTO Keyboard as your active input method.",
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = launchImeOptions,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text("Switch Input Methods")
}
}
}
}

View File

@ -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)
)
*/
}
}

View File

@ -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)
)
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,129 @@
package org.futo.inputmethod.latin.uix.settings.pages
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import org.futo.inputmethod.latin.uix.DISALLOW_SYMBOLS
import org.futo.inputmethod.latin.uix.ENABLE_SOUND
import org.futo.inputmethod.latin.uix.ENGLISH_MODEL_INDEX
import org.futo.inputmethod.latin.uix.SettingsKey
import org.futo.inputmethod.latin.uix.VERBOSE_PROGRESS
import org.futo.inputmethod.latin.uix.settings.ScreenTitle
import org.futo.inputmethod.latin.uix.settings.ScrollableList
import org.futo.inputmethod.latin.uix.settings.SettingToggleDataStore
import org.futo.inputmethod.latin.uix.settings.useDataStore
import org.futo.voiceinput.shared.ENGLISH_MODELS
import org.futo.voiceinput.shared.types.ModelLoader
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ModelPicker(label: String, options: List<ModelLoader>, setting: SettingsKey<Int>) {
val (modelIndex, setModelIndex) = useDataStore(key = setting.key, default = setting.default)
var expanded by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = !expanded
},
modifier = Modifier.align(Alignment.Center)
) {
TextField(
readOnly = true,
value = stringResource(options[modelIndex].name),
onValueChange = { },
label = { Text(label) },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
colors = ExposedDropdownMenuDefaults.textFieldColors(
focusedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,
focusedLeadingIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
focusedIndicatorColor = MaterialTheme.colorScheme.onPrimaryContainer,
focusedTrailingIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
),
modifier = Modifier.menuAnchor()
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
options.forEachIndexed { i, selectionOption ->
DropdownMenuItem(
text = {
Text(stringResource(selectionOption.name))
},
onClick = {
setModelIndex(i)
expanded = false
}
)
}
}
}
}
}
@Preview
@Composable
fun VoiceInputScreen(navController: NavHostController = rememberNavController()) {
val context = LocalContext.current
ScrollableList {
ScreenTitle("Voice Input", showBack = true, navController)
SettingToggleDataStore(
title = "Indication sounds",
subtitle = "Play sounds on start and cancel",
setting = ENABLE_SOUND
)
SettingToggleDataStore(
title = "Verbose progress",
subtitle = "Display verbose information about model inference",
setting = VERBOSE_PROGRESS
)
SettingToggleDataStore(
title = "Suppress symbols",
setting = DISALLOW_SYMBOLS
)
ModelPicker(
"English Model Option",
ENGLISH_MODELS,
ENGLISH_MODEL_INDEX
)
}
}

View File

@ -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(

View File

@ -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,
)

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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 {}
}
}
}
}

View File

@ -0,0 +1,431 @@
package org.futo.inputmethod.latin.uix.voiceinput.downloader
// TODO: Rework this
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.uix.THEME_KEY
import org.futo.inputmethod.latin.uix.deferGetSetting
import org.futo.inputmethod.latin.uix.settings.useDataStore
import org.futo.inputmethod.latin.uix.theme.ThemeOption
import org.futo.inputmethod.latin.uix.theme.ThemeOptions
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme
import org.futo.voiceinput.shared.ui.theme.Typography
import java.io.File
import java.io.IOException
data class ModelInfo(
val name: String,
val url: String,
var size: Long?,
var progress: Float = 0.0f,
var error: Boolean = false,
var finished: Boolean = false
)
val EXAMPLE_MODELS = listOf(
ModelInfo(
name = "tiny-encoder-xatn.tflite",
url = "example.com",
size = 56L * 1024L * 1024L,
progress = 0.5f,
error = true
),
ModelInfo(
name = "tiny-decoder.tflite",
url = "example.com",
size = 73L * 1024L * 1024L,
progress = 0.3f,
error = false
),
)
@Composable
fun ModelItem(model: ModelInfo, showProgress: Boolean) {
Column(modifier = Modifier.padding(4.dp)) {
val color = if (model.error) {
MaterialTheme.colorScheme.errorContainer
} else {
MaterialTheme.colorScheme.primaryContainer
}
Surface(modifier = Modifier, color = color, shape = RoundedCornerShape(4.dp)) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
if (model.error) {
Icon(
Icons.Default.Warning, contentDescription = "Failed", modifier = Modifier
.align(CenterVertically)
.padding(4.dp)
)
}
val size = if (model.size != null) {
"%.1f".format(model.size!!.toFloat() / 1000000.0f)
} else {
"?"
}
Column {
Text(model.name, style = Typography.bodyLarge)
Text(
"$size MB",
style = Typography.bodySmall,
color = MaterialTheme.colorScheme.outline
)
if (showProgress && !model.error) {
LinearProgressIndicator(
progress = model.progress, modifier = Modifier
.fillMaxWidth()
.padding(0.dp, 8.dp),
color = MaterialTheme.colorScheme.onPrimary
)
}
}
}
}
}
}
@Composable
fun ScreenTitle(title: String, showBack: Boolean = false, navController: NavHostController = rememberNavController()) {
val rowModifier = if(showBack) {
Modifier
.fillMaxWidth()
.clickable { navController.popBackStack() }
} else {
Modifier.fillMaxWidth()
}
Row(modifier = rowModifier) {
Spacer(modifier = Modifier.width(16.dp))
if(showBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back", modifier = Modifier.align(CenterVertically))
Spacer(modifier = Modifier.width(18.dp))
}
Text(title, style = Typography.titleLarge, modifier = Modifier
.align(CenterVertically)
.padding(0.dp, 16.dp))
}
}
@Composable
fun ScrollableList(content: @Composable () -> Unit) {
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
) {
content()
}
}
@Composable
@Preview
fun DownloadPrompt(
onContinue: () -> Unit = {},
onCancel: () -> Unit = {},
models: List<ModelInfo> = EXAMPLE_MODELS
) {
ScrollableList {
ScreenTitle(title = stringResource(R.string.download_required))
Text(
stringResource(R.string.download_required_body),
style = Typography.bodyMedium
)
Spacer(modifier = Modifier.height(8.dp))
for(model in models) {
ModelItem(model, showProgress = false)
}
Spacer(modifier = Modifier.height(8.dp))
Row {
Button(
onClick = onCancel, colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary,
contentColor = MaterialTheme.colorScheme.onSecondary
), modifier = Modifier
.padding(8.dp)
.weight(1.0f)
) {
Text(stringResource(R.string.cancel))
}
Button(
onClick = onContinue, modifier = Modifier
.padding(8.dp)
.weight(1.5f)
) {
Text(stringResource(R.string.continue_))
}
}
}
}
@Composable
@Preview
fun DownloadScreen(models: List<ModelInfo> = EXAMPLE_MODELS) {
ScrollableList {
ScreenTitle(stringResource(R.string.download_progress))
if (models.any { it.error }) {
Text(
stringResource(R.string.download_failed),
style = Typography.bodyMedium
)
} else {
Text(
stringResource(R.string.download_in_progress),
style = Typography.bodyMedium
)
}
Spacer(modifier = Modifier.height(8.dp))
for(model in models) {
ModelItem(model, showProgress = true)
}
}
}
fun Context.fileNeedsDownloading(file: String): Boolean {
return !File(this.filesDir, file).exists()
}
class DownloadActivity : ComponentActivity() {
private lateinit var modelsToDownload: List<ModelInfo>
private val httpClient = OkHttpClient()
private var isDownloading = false
private val themeOption: MutableState<ThemeOption?> = mutableStateOf(null)
private fun updateContent() {
setContent {
themeOption.value?.let { themeOption ->
val themeIdx = useDataStore(key = THEME_KEY.key, default = themeOption.key)
val theme: ThemeOption = ThemeOptions[themeIdx.value] ?: themeOption
UixThemeWrapper(theme.obtainColors(LocalContext.current)) {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
if (isDownloading) {
DownloadScreen(models = modelsToDownload)
} else {
DownloadPrompt(
onContinue = { startDownload() },
onCancel = { cancel() },
models = modelsToDownload
)
}
}
}
}
}
}
private fun startDownload() {
isDownloading = true
updateContent()
modelsToDownload.forEach {
val request = Request.Builder().method("GET", null).url(it.url).build()
httpClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
it.error = true
updateContent()
}
override fun onResponse(call: Call, response: Response) {
response.body?.source()?.let { source ->
try {
it.size = response.headers["content-length"]!!.toLong()
} catch (e: Exception) {
println("url failed ${it.url}")
println(response.headers)
e.printStackTrace()
}
val fileName = it.name + ".download"
val file =
File.createTempFile(fileName, null, this@DownloadActivity.cacheDir)
val os = file.outputStream()
val buffer = ByteArray(128 * 1024)
var downloaded = 0
while (true) {
val read = source.read(buffer)
if (read == -1) {
break
}
os.write(buffer.sliceArray(0 until read))
downloaded += read
if (it.size != null) {
it.progress = downloaded.toFloat() / it.size!!.toFloat()
}
lifecycleScope.launch {
withContext(Dispatchers.Main) {
updateContent()
}
}
}
it.finished = true
it.progress = 1.0f
os.flush()
os.close()
assert(file.renameTo(File(this@DownloadActivity.filesDir, it.name)))
if (modelsToDownload.all { a -> a.finished }) {
downloadsFinished()
}
}
}
})
}
}
private fun cancel() {
val returnIntent = Intent()
setResult(RESULT_CANCELED, returnIntent)
finish()
}
private fun downloadsFinished() {
val returnIntent = Intent()
setResult(RESULT_OK, returnIntent)
finish()
}
private fun obtainModelSizes() {
modelsToDownload.forEach {
val request =
Request.Builder().method("HEAD", null).header("accept-encoding", "identity")
.url(it.url).build()
httpClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
it.error = true
updateContent()
}
override fun onResponse(call: Call, response: Response) {
try {
it.size = response.headers["content-length"]!!.toLong()
} catch (e: Exception) {
println("url failed ${it.url}")
println(response.headers)
e.printStackTrace()
it.error = true
}
if (response.code != 200) {
println("Bad response code ${response.code}")
it.error = true
}
updateContent()
}
})
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val models = intent.getStringArrayListExtra("models")
?: throw IllegalStateException("intent extra `models` must be specified for DownloadActivity")
modelsToDownload = models.distinct().filter { this.fileNeedsDownloading(it) }.map {
ModelInfo(
name = it,
url = "https://voiceinput.futo.org/VoiceInput/${it}",
size = null,
progress = 0.0f
)
}
if (modelsToDownload.isEmpty()) {
cancel()
}
isDownloading = false
deferGetSetting(THEME_KEY) {
val themeOptionFromSettings = ThemeOptions[it]
val themeOption = when {
themeOptionFromSettings == null -> VoiceInputTheme
!themeOptionFromSettings.available(this) -> VoiceInputTheme
else -> themeOptionFromSettings
}
this.themeOption.value = themeOption
}
updateContent()
obtainModelSizes()
}
}