From 2088909f8860bad86ae27944e0f9c2de733a3106 Mon Sep 17 00:00:00 2001 From: Aleksandras Kostarevas Date: Mon, 4 Sep 2023 17:30:18 +0300 Subject: [PATCH] Add model downloader activity for voice input --- build.gradle | 2 + java/AndroidManifest.xml | 8 + java/res/values/strings-uix.xml | 10 + .../inputmethod/latin/LatinIMELegacy.java | 8 +- .../latin/settings/SettingsActivity.java | 88 ---- .../latin/settings/SettingsFragment.java | 1 + .../latin/setup/SetupWizardActivity.java | 6 +- .../latin/uix/actions/VoiceInputAction.kt | 20 +- .../voiceinput/downloader/DownloadActivity.kt | 431 ++++++++++++++++++ 9 files changed, 475 insertions(+), 99 deletions(-) delete mode 100644 java/src/org/futo/inputmethod/latin/settings/SettingsActivity.java create mode 100644 java/src/org/futo/inputmethod/latin/uix/voiceinput/downloader/DownloadActivity.kt 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 b4084ab5a..d016de755 100644 --- a/java/AndroidManifest.xml +++ b/java/AndroidManifest.xml @@ -100,6 +100,14 @@ + + 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/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/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/actions/VoiceInputAction.kt b/java/src/org/futo/inputmethod/latin/uix/actions/VoiceInputAction.kt index 2aa629f12..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,5 +1,7 @@ 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 @@ -12,6 +14,7 @@ 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 @@ -34,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 @@ -155,10 +159,18 @@ private class VoiceInputActionWindow( @Composable private fun ModelDownloader(modelException: ModelDoesNotExistException) { - Column { - Text("Model Download Required") - Text("Not yet implemented") - // TODO + 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)) } } 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() + } +}