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.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<SettingsLanguage> {
|
||||
SettingsLanguagePage(
|
||||
onBack = { NavigationController.goBack() },
|
||||
viewModel = viewModel { SettingsViewModel() },
|
||||
)
|
||||
}
|
||||
entry<SettingsChat> {
|
||||
SettingsChatPage(
|
||||
onBack = { NavigationController.goBack() },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user