diff --git a/app/src/main/java/com/m57/hermescontrol/Navigation.kt b/app/src/main/java/com/m57/hermescontrol/Navigation.kt index 368e87cf..5f8d5530 100644 --- a/app/src/main/java/com/m57/hermescontrol/Navigation.kt +++ b/app/src/main/java/com/m57/hermescontrol/Navigation.kt @@ -56,6 +56,7 @@ import com.m57.hermescontrol.ui.settings.SettingsAppearancePage import com.m57.hermescontrol.ui.settings.SettingsBehaviorPage import com.m57.hermescontrol.ui.settings.SettingsChatPage import com.m57.hermescontrol.ui.settings.SettingsConnectionPage +import com.m57.hermescontrol.ui.settings.SettingsLanguagePage import com.m57.hermescontrol.ui.settings.SettingsViewModel import com.m57.hermescontrol.ui.toolsets.ToolsetDetailScreen import kotlinx.coroutines.launch @@ -113,6 +114,12 @@ private fun appEntryProvider( viewModel = viewModel { SettingsViewModel() }, ) } + entry { + SettingsLanguagePage( + onBack = { NavigationController.goBack() }, + viewModel = viewModel { SettingsViewModel() }, + ) + } entry { SettingsChatPage( onBack = { NavigationController.goBack() }, diff --git a/app/src/main/java/com/m57/hermescontrol/NavigationKeys.kt b/app/src/main/java/com/m57/hermescontrol/NavigationKeys.kt index e54bc57a..53ec6d52 100644 --- a/app/src/main/java/com/m57/hermescontrol/NavigationKeys.kt +++ b/app/src/main/java/com/m57/hermescontrol/NavigationKeys.kt @@ -75,6 +75,8 @@ import kotlinx.serialization.Serializable @Serializable data object SettingsAppearance : NavKey +@Serializable data object SettingsLanguage : NavKey + @Serializable data object SettingsChat : NavKey @Serializable data object SettingsBehavior : NavKey diff --git a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsScreen.kt index 071aca2e..e1c532ce 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.automirrored.filled.ListAlt import androidx.compose.material.icons.filled.ChatBubbleOutline import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.Palette import androidx.compose.material.icons.filled.Tune import androidx.compose.material3.Card @@ -39,9 +40,11 @@ import com.m57.hermescontrol.SettingsAppearance import com.m57.hermescontrol.SettingsBehavior import com.m57.hermescontrol.SettingsChat import com.m57.hermescontrol.SettingsConnection +import com.m57.hermescontrol.SettingsLanguage import com.m57.hermescontrol.theme.ThemePreference import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.NavIcon +import com.m57.hermescontrol.ui.settings.components.languageLabel @Composable fun SettingsScreen( @@ -87,6 +90,12 @@ fun SettingsScreen( summary = appearanceSummary(state), onClick = { NavigationController.navigateTo(SettingsAppearance) }, ), + SettingsRow( + icon = Icons.Filled.Language, + label = stringResource(R.string.settings_sec_language), + summary = languageLabel(state.appLanguage), + onClick = { NavigationController.navigateTo(SettingsLanguage) }, + ), SettingsRow( icon = Icons.Filled.ChatBubbleOutline, label = stringResource(R.string.settings_sec_chat), @@ -121,22 +130,12 @@ fun SettingsScreen( } @Composable -private fun appearanceSummary(state: SettingsUiState): String { - val theme = - when (state.themePreference) { - ThemePreference.SYSTEM -> stringResource(R.string.settings_summary_theme_system) - ThemePreference.LIGHT -> stringResource(R.string.settings_summary_theme_light) - ThemePreference.DARK -> stringResource(R.string.settings_summary_theme_dark) - } - val lang = - when (state.appLanguage) { - "system" -> stringResource(R.string.language_system) - "zh" -> stringResource(R.string.language_chinese) - "ko" -> stringResource(R.string.language_korean) - else -> stringResource(R.string.language_english) - } - return "$theme · $lang" -} +private fun appearanceSummary(state: SettingsUiState): String = + when (state.themePreference) { + ThemePreference.SYSTEM -> stringResource(R.string.settings_summary_theme_system) + ThemePreference.LIGHT -> stringResource(R.string.settings_summary_theme_light) + ThemePreference.DARK -> stringResource(R.string.settings_summary_theme_dark) + } private data class SettingsRow( val icon: ImageVector, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsSubPages.kt b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsSubPages.kt index 5ef5bbdd..a7ffd54f 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsSubPages.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsSubPages.kt @@ -36,6 +36,7 @@ import com.m57.hermescontrol.ui.settings.components.AppearanceSection import com.m57.hermescontrol.ui.settings.components.BehaviorSection import com.m57.hermescontrol.ui.settings.components.ChatSection import com.m57.hermescontrol.ui.settings.components.ConnectionSection +import com.m57.hermescontrol.ui.settings.components.LanguageSection import com.m57.hermescontrol.ui.settings.components.TestConnectionButton import com.m57.hermescontrol.ui.settings.components.TestResultCard @@ -132,6 +133,32 @@ internal fun SettingsAppearancePage( onUseDynamicColorsChange = viewModel::onUseDynamicColorsChange, themePreset = state.themePreset, onThemePresetChange = viewModel::onThemePresetChange, + ) + } + } +} + +@Composable +internal fun SettingsLanguagePage( + onBack: () -> Unit, + viewModel: SettingsViewModel = viewModel { SettingsViewModel() }, +) { + val state by viewModel.uiState.collectAsStateWithLifecycle() + + HermesScaffold( + title = { Text(stringResource(R.string.settings_sec_language)) }, + navigationIcon = NavIcon.Back(onBack), + drawerGesturesEnabled = false, + ) { + Column( + modifier = + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + LanguageSection( appLanguage = state.appLanguage, onAppLanguageChange = viewModel::onAppLanguageChange, ) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/settings/components/AppearanceSection.kt b/app/src/main/java/com/m57/hermescontrol/ui/settings/components/AppearanceSection.kt index 6248843e..9df266d1 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/settings/components/AppearanceSection.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/settings/components/AppearanceSection.kt @@ -1,6 +1,5 @@ package com.m57.hermescontrol.ui.settings.components -import androidx.activity.compose.LocalActivity import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -35,7 +34,6 @@ import com.m57.hermescontrol.R import com.m57.hermescontrol.theme.ThemePreference import com.m57.hermescontrol.theme.ThemePreset import com.m57.hermescontrol.ui.settings.SectionCard -import com.m57.hermescontrol.util.LocaleContextWrapper @Composable internal fun AppearanceSection( @@ -45,8 +43,6 @@ internal fun AppearanceSection( onUseDynamicColorsChange: (Boolean) -> Unit, themePreset: ThemePreset, onThemePresetChange: (ThemePreset) -> Unit, - appLanguage: String, - onAppLanguageChange: (String) -> Unit, ) { SectionCard { Text( @@ -189,43 +185,6 @@ internal fun AppearanceSection( } } } - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = stringResource(R.string.settings_item_language), - style = MaterialTheme.typography.bodyLarge, - ) - Spacer(modifier = Modifier.height(8.dp)) - val languageOptions = - listOf( - LocaleContextWrapper.SYSTEM_LANGUAGE to stringResource(R.string.language_system), - "en" to stringResource(R.string.language_english), - "zh" to stringResource(R.string.language_chinese), - "ko" to stringResource(R.string.language_korean), - ) - SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { - val activity = LocalActivity.current - languageOptions.forEachIndexed { index, (code, label) -> - SegmentedButton( - selected = appLanguage == code, - onClick = { - onAppLanguageChange(code) - // MainActivity is a plain ComponentActivity (not - // AppCompatActivity), so the locale only takes effect - // after the activity is recreated. - activity?.recreate() - }, - shape = - SegmentedButtonDefaults.itemShape( - index = index, - count = languageOptions.size, - ), - ) { - Text(label) - } - } - } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/settings/components/LanguageSection.kt b/app/src/main/java/com/m57/hermescontrol/ui/settings/components/LanguageSection.kt new file mode 100644 index 00000000..0eab7cdc --- /dev/null +++ b/app/src/main/java/com/m57/hermescontrol/ui/settings/components/LanguageSection.kt @@ -0,0 +1,90 @@ +package com.m57.hermescontrol.ui.settings.components + +import androidx.activity.compose.LocalActivity +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.m57.hermescontrol.R +import com.m57.hermescontrol.ui.settings.SectionCard +import com.m57.hermescontrol.util.LocaleContextWrapper + +/** + * Supported languages as (code, display label) pairs. Add a new language + * here (plus its label in strings.xml) and both the picker and the summary + * adapt automatically. + */ +@Composable +internal fun supportedLanguages(): List> = + listOf( + LocaleContextWrapper.SYSTEM_LANGUAGE to stringResource(R.string.language_system), + "en" to stringResource(R.string.language_english), + "zh" to stringResource(R.string.language_chinese), + "ko" to stringResource(R.string.language_korean), + ) + +/** Display label for a language code, falling back to English for unknown codes. */ +@Composable +internal fun languageLabel(code: String): String = + supportedLanguages().firstOrNull { it.first == code }?.second + ?: stringResource(R.string.language_english) + +@Composable +internal fun LanguageSection( + appLanguage: String, + onAppLanguageChange: (String) -> Unit, +) { + SectionCard { + Text( + text = stringResource(R.string.settings_item_language), + style = MaterialTheme.typography.bodyLarge, + ) + Spacer(modifier = Modifier.height(8.dp)) + + val languageOptions = supportedLanguages() + val activity = LocalActivity.current + var expanded by remember { mutableStateOf(false) } + Box(modifier = Modifier.fillMaxWidth()) { + OutlinedButton( + onClick = { expanded = true }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(languageLabel(appLanguage)) + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.fillMaxWidth(0.85f), + ) { + languageOptions.forEach { (code, label) -> + DropdownMenuItem( + text = { Text(label) }, + onClick = { + expanded = false + if (code != appLanguage) { + onAppLanguageChange(code) + // MainActivity is a plain ComponentActivity + // (not AppCompatActivity), so the locale only + // takes effect after the activity is recreated. + activity?.recreate() + } + }, + ) + } + } + } + } +} diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index a81f0bda..e61135fe 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -146,6 +146,7 @@ 100 毫秒 测试连接 关于 + 语言 Hermes Mobile 版本 构建 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b56533a1..b68bbf31 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -169,6 +169,7 @@ 100 ms Test Connection About + Language Hermes Mobile Version Build