refactor: remove bottom navigation bar (#665 #666 #667 #668) (#672)

* refactor(nav): remove bottom NavigationBar from Scaffold (#665)

* refactor(theme): remove BottomNavDisplayMode enum (#666)

* refactor(settings): remove nav bar customization page (#667)

* chore: clean up unused imports after bottom nav removal (#668)

* chore: remove dead bottom nav infrastructure after removal

- Remove SettingsNavBar NavKey (NavigationKeys.kt)
- Remove bottomNavItems field from ServerStoreState + migration code
- Remove _bottomNavItemsFlow, getBottomNavItems, setBottomNavItems from AuthManager
- Remove bottomNavItems handling from selfHealed() (ServerCollectionOps)
- Remove redundant bottomBar={} from Scaffold (Navigation.kt)
- Remove unused nav bar strings from values/strings.xml + values-ko/strings.xml
- Remove content_desc_move_up/down/remove_item strings
- Update SettingsViewModelTest.kt (drop stale getBottomNavItems mock)
- Update ServerStoreTest.kt (remove bottomNavItems assertions)

---------

Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-07-22 10:14:50 +03:00
1 parent c43904b2b8
commit 0edb67338d
17 files changed
-580

No files matched your search

@@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -22,9 +21,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.NavigationDrawerItemDefaults
import androidx.compose.material3.Scaffold
@@ -32,15 +28,12 @@ import androidx.compose.material3.Text
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -52,7 +45,6 @@ import androidx.navigation3.ui.NavDisplay
import com.m57.hermescontrol.data.local.AuthManager
import com.m57.hermescontrol.data.ws.ConnectionStatus
import com.m57.hermescontrol.data.ws.HermesWsClient
import com.m57.hermescontrol.theme.BottomNavDisplayMode
import com.m57.hermescontrol.theme.LocalHermesStatusColors
import com.m57.hermescontrol.ui.common.DisableDrawerGestures
import com.m57.hermescontrol.ui.common.DrawerGestureController
@@ -62,46 +54,11 @@ 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.SettingsNavBarPage
import com.m57.hermescontrol.ui.settings.SettingsViewModel
import kotlinx.coroutines.launch
import com.m57.hermescontrol.ui.authlogin.AuthLoginScreen as AuthLoginScreenContent
import com.m57.hermescontrol.ui.landing.LandingScreen as LandingScreenContent
// NOTE: if a new Settings drill-down screen is added, its NavKey MUST be added
// here too, or the bottom-nav Settings highlight will drop on that sub-page.
// (Keys are flat data objects, not a sealed hierarchy, so no type-based check
// is possible without a larger refactor — see issue #637.)
private val settingsDestinations: Set<NavKey> =
setOf(
SettingsScreen,
SettingsConnection,
SettingsAppearance,
SettingsChat,
SettingsNavBar,
SettingsBehavior,
SettingsAbout,
)
/**
* Keep Settings highlighted in the bottom nav while one of its drill-down
* sub-pages is visible. Navigation is unaffected — this only maps the
* *selection* key back to the Settings parent so the indicator doesn't drop.
*/
internal fun selectedBottomNavDestination(currentScreen: NavKey): NavKey =
if (currentScreen in settingsDestinations) SettingsScreen else currentScreen
/** Maps a compact bottom-nav display mode to its bar height. */
internal fun Modifier.bottomNavigationHeight(displayMode: BottomNavDisplayMode): Modifier =
when (displayMode) {
BottomNavDisplayMode.ICON_ONLY -> height(56.dp)
BottomNavDisplayMode.TEXT_ONLY -> height(44.dp)
BottomNavDisplayMode.ICON_AND_TEXT -> heightIn(min = 80.dp)
}
private fun resolveBottomNavItems(names: List<String>): List<ScreenDefinition> =
names.mapNotNull { name -> ScreenRegistry.ALL_SCREENS.firstOrNull { it.key::class.simpleName == name } }
private fun appEntryProvider(
sessionId: String?,
openDrawer: () -> Unit,
@@ -159,12 +116,6 @@ private fun appEntryProvider(
viewModel = viewModel { SettingsViewModel() },
)
}
entry<SettingsNavBar> {
SettingsNavBarPage(
onBack = { NavigationController.goBack() },
viewModel = viewModel { SettingsViewModel() },
)
}
entry<SettingsBehavior> {
SettingsBehaviorPage(
onBack = { NavigationController.goBack() },
@@ -189,7 +140,6 @@ fun MainNavigation(sessionId: String? = null) {
NavigationController.backStack = backStack
val currentScreen = backStack.lastOrNull() ?: startScreen
val selectedDestination = selectedBottomNavDestination(currentScreen)
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
@@ -204,19 +154,6 @@ fun MainNavigation(sessionId: String? = null) {
DrawerGestureController(drawerState, scope)
}
val bottomNavItemsState by AuthManager.bottomNavItemsFlow.collectAsState()
val bottomNavDisplayMode by AuthManager.bottomNavDisplayModeFlow.collectAsState()
val bottomNavItems = resolveBottomNavItems(bottomNavItemsState)
val bottomNavKeys = remember(bottomNavItems) { bottomNavItems.mapTo(mutableSetOf()) { it.key } }
LaunchedEffect(bottomNavKeys) {
NavigationController.updatePrimaryScreens(bottomNavKeys)
}
val showBottomBar =
currentScreen != LandingScreen &&
currentScreen != AuthLoginScreen
val openDrawer: () -> Unit = { scope.launch { drawerState.open() } }
CompositionLocalProvider(LocalDrawerGestureController provides gestureController) {
@@ -312,60 +249,6 @@ fun MainNavigation(sessionId: String? = null) {
) {
Scaffold(
contentWindowInsets = WindowInsets.navigationBars,
bottomBar = {
if (showBottomBar) {
NavigationBar(
modifier = Modifier.bottomNavigationHeight(bottomNavDisplayMode),
) {
bottomNavItems.forEach { item ->
val showIcon =
bottomNavDisplayMode == BottomNavDisplayMode.ICON_AND_TEXT ||
bottomNavDisplayMode == BottomNavDisplayMode.ICON_ONLY
val showLabel =
bottomNavDisplayMode == BottomNavDisplayMode.ICON_AND_TEXT ||
bottomNavDisplayMode == BottomNavDisplayMode.TEXT_ONLY
val isSelected = selectedDestination == item.key
NavigationBarItem(
selected = isSelected,
onClick = { NavigationController.navigateTo(item.key) },
colors =
if (bottomNavDisplayMode == BottomNavDisplayMode.TEXT_ONLY) {
NavigationBarItemDefaults.colors(
indicatorColor = Color.Transparent,
)
} else {
NavigationBarItemDefaults.colors()
},
icon = {
if (showIcon) {
Icon(item.icon, contentDescription = stringResource(item.labelRes))
} else {
Text(
text = stringResource(item.labelRes),
style = MaterialTheme.typography.labelMedium,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
)
}
},
label =
if (showLabel && showIcon) {
{ Text(stringResource(item.labelRes)) }
} else {
null
},
modifier =
Modifier.testTag(
"nav_${item.key::class.simpleName?.lowercase()?.removeSuffix(
"screen",
) ?: ""}",
),
)
}
}
}
},
) { paddingValues ->
NavDisplay(
backStack = backStack,
@@ -61,8 +61,6 @@ import kotlinx.serialization.Serializable
@Serializable data object SettingsChat : NavKey
@Serializable data object SettingsNavBar : NavKey
@Serializable data object SettingsBehavior : NavKey
@Serializable data object SettingsAbout : NavKey
@@ -20,17 +20,8 @@ fun ServerStoreState.selfHealed(): ServerStoreState {
val hasActive = connectionProfiles.any { it.id == selectedProfileId }
val newSelected = if (selectedProfileId != null && !hasActive) null else selectedProfileId
val validItems = bottomNavItems.filter { it.isNotBlank() }
val finalBottomNavItems =
if (validItems.isEmpty()) {
listOf("ChatScreen", "SkillsScreen", "CronJobsScreen", "SystemScreen", "SettingsScreen")
} else {
validItems
}
return copy(
selectedProfileId = newSelected,
bottomNavItems = finalBottomNavItems,
)
}
@@ -8,7 +8,6 @@ import androidx.security.crypto.MasterKeys
import com.m57.hermescontrol.data.model.PinnedModel
import com.m57.hermescontrol.data.remote.OkHttpProvider
import com.m57.hermescontrol.data.remote.ServerEndpoint
import com.m57.hermescontrol.theme.BottomNavDisplayMode
import com.m57.hermescontrol.theme.ThemePreference
import com.m57.hermescontrol.theme.ThemePreset
import kotlinx.serialization.decodeFromString
@@ -27,8 +26,6 @@ class ServerStoreMigration(
"theme_preference",
"use_dynamic_colors",
"theme_preset",
"bottom_nav_display_mode",
"bottom_nav_items",
"connection_profiles",
"selected_profile_id",
"pinned_models",
@@ -65,20 +62,6 @@ class ServerStoreMigration(
runCatching { ThemePreset.valueOf(themePresetString!!) }
.getOrDefault(ThemePreset.DEFAULT)
val bottomNavDisplayModeString =
prefs.getString(
"bottom_nav_display_mode",
BottomNavDisplayMode.ICON_AND_TEXT.name,
)
val bottomNavDisplayMode =
runCatching { BottomNavDisplayMode.valueOf(bottomNavDisplayModeString!!) }
.getOrDefault(BottomNavDisplayMode.ICON_AND_TEXT)
val bottomNavItemsRaw = prefs.getString("bottom_nav_items", null)
val bottomNavItems =
bottomNavItemsRaw?.split(",")?.filter { it.isNotBlank() }
?: listOf("ChatScreen", "SkillsScreen", "CronJobsScreen", "SystemScreen", "SettingsScreen")
val connectionProfilesRaw = prefs.getString("connection_profiles", null)
val connectionProfiles =
if (connectionProfilesRaw != null) {
@@ -117,8 +100,6 @@ class ServerStoreMigration(
themePreference = themePreference,
useDynamicColors = useDynamicColors,
themePreset = themePreset,
bottomNavDisplayMode = bottomNavDisplayMode,
bottomNavItems = bottomNavItems,
connectionProfiles =
connectionProfiles.map { profile ->
profile.copy(
@@ -1,7 +1,6 @@
package com.m57.hermescontrol.data.config
import com.m57.hermescontrol.data.model.PinnedModel
import com.m57.hermescontrol.theme.BottomNavDisplayMode
import com.m57.hermescontrol.theme.ThemePreference
import com.m57.hermescontrol.theme.ThemePreset
import kotlinx.serialization.Serializable
@@ -16,9 +15,6 @@ data class ServerStoreState(
val themePreference: ThemePreference = ThemePreference.SYSTEM,
val useDynamicColors: Boolean = true,
val themePreset: ThemePreset = ThemePreset.DEFAULT,
val bottomNavDisplayMode: BottomNavDisplayMode = BottomNavDisplayMode.ICON_AND_TEXT,
val bottomNavItems: List<String> =
listOf("ChatScreen", "SkillsScreen", "CronJobsScreen", "SystemScreen", "SettingsScreen"),
val connectionProfiles: List<ConnectionProfile> = emptyList(),
val selectedProfileId: String? = null,
val pinnedModels: List<PinnedModel> = emptyList(),
@@ -16,7 +16,6 @@ import com.m57.hermescontrol.data.model.PinnedModel
import com.m57.hermescontrol.data.remote.CleartextPolicy
import com.m57.hermescontrol.data.remote.CookieManager
import com.m57.hermescontrol.data.remote.ServerEndpoint
import com.m57.hermescontrol.theme.BottomNavDisplayMode
import com.m57.hermescontrol.theme.ThemePreference
import com.m57.hermescontrol.theme.ThemePreset
import kotlinx.coroutines.CoroutineScope
@@ -61,9 +60,6 @@ object AuthManager {
"AuthManager not initialized. Call init(context) first.",
)
private val _bottomNavItemsFlow = MutableStateFlow<List<String>>(emptyList())
val bottomNavItemsFlow: StateFlow<List<String>> = _bottomNavItemsFlow.asStateFlow()
private val _themePreferenceFlow = MutableStateFlow<ThemePreference>(ThemePreference.SYSTEM)
val themePreferenceFlow: StateFlow<ThemePreference> = _themePreferenceFlow.asStateFlow()
@@ -73,9 +69,6 @@ object AuthManager {
private val _themePresetFlow = MutableStateFlow<ThemePreset>(ThemePreset.DEFAULT)
val themePresetFlow: StateFlow<ThemePreset> = _themePresetFlow.asStateFlow()
private val _bottomNavDisplayModeFlow = MutableStateFlow<BottomNavDisplayMode>(BottomNavDisplayMode.ICON_AND_TEXT)
val bottomNavDisplayModeFlow: StateFlow<BottomNavDisplayMode> = _bottomNavDisplayModeFlow.asStateFlow()
private val _tokenFlow = MutableStateFlow<String?>(null)
val tokenFlow: StateFlow<String?> = _tokenFlow.asStateFlow()
@@ -133,11 +126,9 @@ object AuthManager {
scope.launch {
store.stateFlow.collect { state ->
_bottomNavItemsFlow.value = state.bottomNavItems
_themePreferenceFlow.value = state.themePreference
_useDynamicColorsFlow.value = state.useDynamicColors
_themePresetFlow.value = state.themePreset
_bottomNavDisplayModeFlow.value = state.bottomNavDisplayMode
// B7 (Jul 08 2026, kanban t_470): keep cookie scope aligned with active profile.
syncCookieStoreForProfile(state.selectedProfileId)
}
@@ -512,16 +503,6 @@ object AuthManager {
)
}
// ── Bottom nav bar items ──────────────────────────────────────────────
/** Returns the list of selected bottom-nav item keys (data-object names). */
fun getBottomNavItems(): List<String> = serverStore.getLatestState().bottomNavItems
/** Persist the ordered list of bottom-nav item keys (max 5, data-object names). */
fun setBottomNavItems(items: List<String>) {
serverStore.update { it.copy(bottomNavItems = items) }
}
// ── Typing Effect ───────────────────────────────────────────────────
fun isTypingEffectEnabled(): Boolean = serverStore.getLatestState().typingEffectEnabled
@@ -544,12 +525,4 @@ object AuthManager {
fun setTypingEffectDelayMs(delayMs: Int) {
serverStore.update { it.copy(typingEffectDelayMs = delayMs) }
}
// ── Bottom Nav Display Mode ──────────────────────────────────────────
fun getBottomNavDisplayMode(): BottomNavDisplayMode = serverStore.getLatestState().bottomNavDisplayMode
fun setBottomNavDisplayMode(mode: BottomNavDisplayMode) {
serverStore.update { it.copy(bottomNavDisplayMode = mode) }
}
}
@@ -41,9 +41,6 @@ enum class ThemePreference { SYSTEM, LIGHT, DARK }
@Serializable
enum class ThemePreset { DEFAULT, MONOCHROME, GRUVBOX, CATPPUCCIN, AMOLED, NEON_NOIR }
@Serializable
enum class BottomNavDisplayMode { ICON_AND_TEXT, ICON_ONLY, TEXT_ONLY }
val LocalThemePreference = compositionLocalOf { ThemePreference.SYSTEM }
val LocalThemePreset = compositionLocalOf { ThemePreset.DEFAULT }
@@ -11,7 +11,6 @@ import androidx.compose.material.icons.Icons
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.Dashboard
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Tune
@@ -40,7 +39,6 @@ import com.m57.hermescontrol.SettingsAppearance
import com.m57.hermescontrol.SettingsBehavior
import com.m57.hermescontrol.SettingsChat
import com.m57.hermescontrol.SettingsConnection
import com.m57.hermescontrol.SettingsNavBar
import com.m57.hermescontrol.theme.ThemePreference
import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.NavIcon
@@ -100,11 +98,6 @@ fun SettingsScreen(
},
onClick = { NavigationController.navigateTo(SettingsChat) },
),
SettingsRow(
icon = Icons.Filled.Dashboard,
label = stringResource(R.string.settings_sec_nav_bar),
onClick = { NavigationController.navigateTo(SettingsNavBar) },
),
SettingsRow(
icon = Icons.Filled.Tune,
label = stringResource(R.string.settings_sec_behavior),
@@ -31,7 +31,6 @@ 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.NavBarSection
import com.m57.hermescontrol.ui.settings.components.TestConnectionButton
import com.m57.hermescontrol.ui.settings.components.TestResultCard
@@ -165,39 +164,6 @@ internal fun SettingsChatPage(
}
}
@Composable
internal fun SettingsNavBarPage(
onBack: () -> Unit,
viewModel: SettingsViewModel = viewModel { SettingsViewModel() },
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
HermesScaffold(
title = { Text(stringResource(R.string.settings_sec_nav_bar)) },
navigationIcon = NavIcon.Back(onBack),
drawerGesturesEnabled = false,
) {
Column(
modifier =
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
NavBarSection(
bottomNavDisplayMode = state.bottomNavDisplayMode,
onBottomNavDisplayModeChange = viewModel::onBottomNavDisplayModeChange,
selectedNavItems = state.selectedNavItems,
availableNavItems = viewModel.availableNavItems,
onReorderNavItems = viewModel::reorderNavItems,
onRemoveNavItem = viewModel::removeNavItem,
onAddNavItem = viewModel::addNavItem,
)
}
}
}
@Composable
internal fun SettingsBehaviorPage(
onBack: () -> Unit,
@@ -2,7 +2,6 @@ package com.m57.hermescontrol.ui.settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.m57.hermescontrol.ScreenRegistry
import com.m57.hermescontrol.data.config.ConnectionProfile
import com.m57.hermescontrol.data.config.resolveBaseUrl
import com.m57.hermescontrol.data.local.AuthManager
@@ -12,7 +11,6 @@ import com.m57.hermescontrol.data.remote.NetworkResult
import com.m57.hermescontrol.data.remote.ServerEndpoint
import com.m57.hermescontrol.data.remote.safeApiCall
import com.m57.hermescontrol.data.ws.HermesWsClient
import com.m57.hermescontrol.theme.BottomNavDisplayMode
import com.m57.hermescontrol.theme.ThemePreference
import com.m57.hermescontrol.theme.ThemePreset
import kotlinx.coroutines.CoroutineDispatcher
@@ -35,13 +33,11 @@ data class SettingsUiState(
val isTesting: Boolean = false,
val testResult: String? = null,
val isSaved: Boolean = false,
val selectedNavItems: List<String> = emptyList(),
val typingEffectEnabled: Boolean = false,
val typingEffectDelayMs: Int = 30,
val profiles: List<ConnectionProfile> = emptyList(),
val selectedProfileId: String? = null,
val renameProfileName: String = "",
val bottomNavDisplayMode: BottomNavDisplayMode = BottomNavDisplayMode.ICON_AND_TEXT,
val appLanguage: String = "system",
// Profile add/edit dialog
val showProfileDialog: Boolean = false,
@@ -77,11 +73,9 @@ class SettingsViewModel(
val themePreference = AuthManager.getThemePreference()
val useDynamicColors = AuthManager.isUseDynamicColors()
val themePreset = AuthManager.getThemePreset()
val selectedNavItems = AuthManager.getBottomNavItems()
val typingEffectEnabled = AuthManager.isTypingEffectEnabled()
val typingEffectDelayMs = AuthManager.getTypingEffectDelayMs()
val profiles = AuthManager.getConnectionProfiles()
val bottomNavDisplayMode = AuthManager.getBottomNavDisplayMode()
val appLanguage = AuthManager.getAppLanguage()
val renameProfileName =
profiles.firstOrNull { p -> p.id == selectedId }?.name ?: ""
@@ -98,13 +92,11 @@ class SettingsViewModel(
themePreference = themePreference,
useDynamicColors = useDynamicColors,
themePreset = themePreset,
selectedNavItems = selectedNavItems,
typingEffectEnabled = typingEffectEnabled,
typingEffectDelayMs = typingEffectDelayMs,
profiles = profiles,
selectedProfileId = selectedId,
renameProfileName = renameProfileName,
bottomNavDisplayMode = bottomNavDisplayMode,
appLanguage = appLanguage,
)
}
@@ -312,11 +304,6 @@ class SettingsViewModel(
AuthManager.setAppLanguage(code)
}
fun onBottomNavDisplayModeChange(mode: BottomNavDisplayMode) {
_uiState.update { it.copy(bottomNavDisplayMode = mode, isSaved = false) }
AuthManager.setBottomNavDisplayMode(mode)
}
fun onTypingEffectEnabledChange(enabled: Boolean) {
_uiState.update { it.copy(typingEffectEnabled = enabled, isSaved = false) }
AuthManager.setTypingEffectEnabled(enabled)
@@ -336,34 +323,6 @@ class SettingsViewModel(
// Don't rebuild ApiClient here — let the navigation complete first
}
/** All screens available for bottom-nav selection (name → display label). */
val availableNavItems: List<Pair<String, Int>> =
ScreenRegistry.ALL_SCREENS.map { screen ->
screen.key::class.simpleName!! to screen.labelRes
}
/** Add a nav item to the bottom bar (max 5). Auto-saves. */
fun addNavItem(name: String) {
val current = _uiState.value.selectedNavItems
if (name in current || current.size >= 5) return
val updated = current + name
_uiState.update { it.copy(selectedNavItems = updated) }
AuthManager.setBottomNavItems(updated)
}
/** Remove a nav item from the bottom bar. Auto-saves. */
fun removeNavItem(name: String) {
val updated = _uiState.value.selectedNavItems - name
_uiState.update { it.copy(selectedNavItems = updated) }
AuthManager.setBottomNavItems(updated)
}
/** Reorder the selected nav items. Auto-saves. */
fun reorderNavItems(items: List<String>) {
_uiState.update { it.copy(selectedNavItems = items) }
AuthManager.setBottomNavItems(items)
}
fun save() {
val state = _uiState.value
val normalized =
@@ -379,7 +338,6 @@ class SettingsViewModel(
AuthManager.setThemePreference(state.themePreference)
AuthManager.setUseDynamicColors(state.useDynamicColors)
AuthManager.setThemePreset(state.themePreset)
AuthManager.setBottomNavDisplayMode(state.bottomNavDisplayMode)
AuthManager.setTypingEffectEnabled(state.typingEffectEnabled)
AuthManager.setTypingEffectDelayMs(state.typingEffectDelayMs)
ApiClient.rebuild()
@@ -11,16 +11,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.SegmentedButton
@@ -40,7 +32,6 @@ 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.theme.BottomNavDisplayMode
import com.m57.hermescontrol.theme.ThemePreference
import com.m57.hermescontrol.theme.ThemePreset
import com.m57.hermescontrol.ui.settings.SectionCard
@@ -315,157 +306,3 @@ internal fun ChatSection(
}
}
}
@Composable
internal fun NavBarSection(
bottomNavDisplayMode: BottomNavDisplayMode,
onBottomNavDisplayModeChange: (BottomNavDisplayMode) -> Unit,
selectedNavItems: List<String>,
availableNavItems: List<Pair<String, Int>>,
onReorderNavItems: (List<String>) -> Unit,
onRemoveNavItem: (String) -> Unit,
onAddNavItem: (String) -> Unit,
) {
SectionCard {
Text(
text = stringResource(R.string.settings_item_bottom_nav_display_mode),
style = MaterialTheme.typography.bodyLarge,
)
Spacer(modifier = Modifier.height(8.dp))
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
BottomNavDisplayMode.entries.forEachIndexed { index, mode ->
SegmentedButton(
selected = bottomNavDisplayMode == mode,
onClick = { onBottomNavDisplayModeChange(mode) },
shape =
SegmentedButtonDefaults.itemShape(
index = index,
count = BottomNavDisplayMode.entries.size,
),
) {
Text(
when (mode) {
BottomNavDisplayMode.ICON_AND_TEXT -> {
stringResource(
R.string.settings_display_mode_icon_and_text,
)
}
BottomNavDisplayMode.ICON_ONLY -> {
stringResource(
R.string.settings_display_mode_icon_only,
)
}
BottomNavDisplayMode.TEXT_ONLY -> {
stringResource(
R.string.settings_display_mode_text_only,
)
}
},
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.settings_item_nav_items, selectedNavItems.size),
style = MaterialTheme.typography.bodyMedium,
)
Spacer(modifier = Modifier.height(8.dp))
selectedNavItems.forEachIndexed { index, name ->
val labelRes = availableNavItems.firstOrNull { it.first == name }?.second
val label = if (labelRes != null) stringResource(labelRes) else name
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = label,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.weight(1f),
)
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
IconButton(
onClick = {
val list = selectedNavItems.toMutableList()
val temp = list[index]
list[index] = list[index - 1]
list[index - 1] = temp
onReorderNavItems(list)
},
enabled = index > 0,
) {
Icon(
Icons.Filled.KeyboardArrowUp,
contentDescription = stringResource(R.string.content_desc_move_up),
)
}
IconButton(
onClick = {
val list = selectedNavItems.toMutableList()
val temp = list[index]
list[index] = list[index + 1]
list[index + 1] = temp
onReorderNavItems(list)
},
enabled = index < selectedNavItems.lastIndex,
) {
Icon(
Icons.Filled.KeyboardArrowDown,
contentDescription = stringResource(R.string.content_desc_move_down),
)
}
IconButton(onClick = { onRemoveNavItem(name) }) {
Icon(
Icons.Filled.Close,
contentDescription =
stringResource(
R.string.content_desc_remove_item,
label,
),
)
}
}
}
}
if (selectedNavItems.size < 5) {
val available =
availableNavItems.filter { it.first !in selectedNavItems }
if (available.isNotEmpty()) {
var expanded by remember { mutableStateOf(false) }
Box {
OutlinedButton(onClick = { expanded = true }) {
Icon(Icons.Filled.Add, contentDescription = null)
Spacer(Modifier.size(4.dp))
Text(stringResource(R.string.settings_action_add_item))
}
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
available.forEach { (name, labelRes) ->
DropdownMenuItem(
text = { Text(stringResource(labelRes)) },
onClick = {
onAddNavItem(name)
expanded = false
},
)
}
}
}
}
} else {
Text(
text = stringResource(R.string.settings_nav_max_reached),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}