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:
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()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in new issue
Block a user