feat(settings): move language to its own section with scalable dropdown picker (#906)

- New Language row in the main Settings list with current-language summary
- New SettingsLanguage drill-down page with an OutlinedButton + DropdownMenu
  picker (scales to any number of languages; same pattern as theme presets)
- Shared supportedLanguages()/languageLabel() helpers — adding a language
  is now one entry in one place
- Removed the 4-option segmented pill row from the Appearance page
- Added settings_sec_language string (en + zh)

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-08-15 12:18:37 +03:00
1 parent fdbd98af3e
commit 063c27b141
8 files changed
+143 -57

No files matched your search

@@ -56,6 +56,7 @@ import com.m57.hermescontrol.ui.settings.SettingsAppearancePage
import com.m57.hermescontrol.ui.settings.SettingsBehaviorPage import com.m57.hermescontrol.ui.settings.SettingsBehaviorPage
import com.m57.hermescontrol.ui.settings.SettingsChatPage import com.m57.hermescontrol.ui.settings.SettingsChatPage
import com.m57.hermescontrol.ui.settings.SettingsConnectionPage 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.settings.SettingsViewModel
import com.m57.hermescontrol.ui.toolsets.ToolsetDetailScreen import com.m57.hermescontrol.ui.toolsets.ToolsetDetailScreen
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -113,6 +114,12 @@ private fun appEntryProvider(
viewModel = viewModel { SettingsViewModel() }, viewModel = viewModel { SettingsViewModel() },
) )
} }
entry<SettingsLanguage> {
SettingsLanguagePage(
onBack = { NavigationController.goBack() },
viewModel = viewModel { SettingsViewModel() },
)
}
entry<SettingsChat> { entry<SettingsChat> {
SettingsChatPage( SettingsChatPage(
onBack = { NavigationController.goBack() }, onBack = { NavigationController.goBack() },
@@ -75,6 +75,8 @@ import kotlinx.serialization.Serializable
@Serializable data object SettingsAppearance : NavKey @Serializable data object SettingsAppearance : NavKey
@Serializable data object SettingsLanguage : NavKey
@Serializable data object SettingsChat : NavKey @Serializable data object SettingsChat : NavKey
@Serializable data object SettingsBehavior : NavKey @Serializable data object SettingsBehavior : NavKey
@@ -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.automirrored.filled.ListAlt
import androidx.compose.material.icons.filled.ChatBubbleOutline import androidx.compose.material.icons.filled.ChatBubbleOutline
import androidx.compose.material.icons.filled.Info 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.Palette
import androidx.compose.material.icons.filled.Tune import androidx.compose.material.icons.filled.Tune
import androidx.compose.material3.Card import androidx.compose.material3.Card
@@ -39,9 +40,11 @@ import com.m57.hermescontrol.SettingsAppearance
import com.m57.hermescontrol.SettingsBehavior import com.m57.hermescontrol.SettingsBehavior
import com.m57.hermescontrol.SettingsChat import com.m57.hermescontrol.SettingsChat
import com.m57.hermescontrol.SettingsConnection import com.m57.hermescontrol.SettingsConnection
import com.m57.hermescontrol.SettingsLanguage
import com.m57.hermescontrol.theme.ThemePreference import com.m57.hermescontrol.theme.ThemePreference
import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.settings.components.languageLabel
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
@@ -87,6 +90,12 @@ fun SettingsScreen(
summary = appearanceSummary(state), summary = appearanceSummary(state),
onClick = { NavigationController.navigateTo(SettingsAppearance) }, 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( SettingsRow(
icon = Icons.Filled.ChatBubbleOutline, icon = Icons.Filled.ChatBubbleOutline,
label = stringResource(R.string.settings_sec_chat), label = stringResource(R.string.settings_sec_chat),
@@ -121,22 +130,12 @@ fun SettingsScreen(
} }
@Composable @Composable
private fun appearanceSummary(state: SettingsUiState): String { private fun appearanceSummary(state: SettingsUiState): String =
val theme = when (state.themePreference) {
when (state.themePreference) { ThemePreference.SYSTEM -> stringResource(R.string.settings_summary_theme_system)
ThemePreference.SYSTEM -> stringResource(R.string.settings_summary_theme_system) ThemePreference.LIGHT -> stringResource(R.string.settings_summary_theme_light)
ThemePreference.LIGHT -> stringResource(R.string.settings_summary_theme_light) ThemePreference.DARK -> stringResource(R.string.settings_summary_theme_dark)
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 data class SettingsRow( private data class SettingsRow(
val icon: ImageVector, val icon: ImageVector,
@@ -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.BehaviorSection
import com.m57.hermescontrol.ui.settings.components.ChatSection import com.m57.hermescontrol.ui.settings.components.ChatSection
import com.m57.hermescontrol.ui.settings.components.ConnectionSection 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.TestConnectionButton
import com.m57.hermescontrol.ui.settings.components.TestResultCard import com.m57.hermescontrol.ui.settings.components.TestResultCard
@@ -132,6 +133,32 @@ internal fun SettingsAppearancePage(
onUseDynamicColorsChange = viewModel::onUseDynamicColorsChange, onUseDynamicColorsChange = viewModel::onUseDynamicColorsChange,
themePreset = state.themePreset, themePreset = state.themePreset,
onThemePresetChange = viewModel::onThemePresetChange, 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, appLanguage = state.appLanguage,
onAppLanguageChange = viewModel::onAppLanguageChange, onAppLanguageChange = viewModel::onAppLanguageChange,
) )
@@ -1,6 +1,5 @@
package com.m57.hermescontrol.ui.settings.components package com.m57.hermescontrol.ui.settings.components
import androidx.activity.compose.LocalActivity
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.ThemePreference
import com.m57.hermescontrol.theme.ThemePreset import com.m57.hermescontrol.theme.ThemePreset
import com.m57.hermescontrol.ui.settings.SectionCard import com.m57.hermescontrol.ui.settings.SectionCard
import com.m57.hermescontrol.util.LocaleContextWrapper
@Composable @Composable
internal fun AppearanceSection( internal fun AppearanceSection(
@@ -45,8 +43,6 @@ internal fun AppearanceSection(
onUseDynamicColorsChange: (Boolean) -> Unit, onUseDynamicColorsChange: (Boolean) -> Unit,
themePreset: ThemePreset, themePreset: ThemePreset,
onThemePresetChange: (ThemePreset) -> Unit, onThemePresetChange: (ThemePreset) -> Unit,
appLanguage: String,
onAppLanguageChange: (String) -> Unit,
) { ) {
SectionCard { SectionCard {
Text( 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)
}
}
}
} }
} }
@@ -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<Pair<String, String>> =
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()
}
},
)
}
}
}
}
}
+1
View File
@@ -146,6 +146,7 @@
<string name="settings_delay_100ms">100 毫秒</string> <string name="settings_delay_100ms">100 毫秒</string>
<string name="settings_action_test_connection">测试连接</string> <string name="settings_action_test_connection">测试连接</string>
<string name="settings_sec_about">关于</string> <string name="settings_sec_about">关于</string>
<string name="settings_sec_language">语言</string>
<string name="settings_about_app_name">Hermes Mobile</string> <string name="settings_about_app_name">Hermes Mobile</string>
<string name="settings_about_version">版本</string> <string name="settings_about_version">版本</string>
<string name="settings_about_build">构建</string> <string name="settings_about_build">构建</string>
+1
View File
@@ -169,6 +169,7 @@
<string name="settings_delay_100ms">100 ms</string> <string name="settings_delay_100ms">100 ms</string>
<string name="settings_action_test_connection">Test Connection</string> <string name="settings_action_test_connection">Test Connection</string>
<string name="settings_sec_about">About</string> <string name="settings_sec_about">About</string>
<string name="settings_sec_language">Language</string>
<string name="settings_about_app_name">Hermes Mobile</string> <string name="settings_about_app_name">Hermes Mobile</string>
<string name="settings_about_version">Version</string> <string name="settings_about_version">Version</string>
<string name="settings_about_build">Build</string> <string name="settings_about_build">Build</string>