From 0edb67338dd8b9f398f09ec1171f81ac5f6d9998 Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Wed, 22 Jul 2026 10:14:50 +0300 Subject: [PATCH] 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 --- .../BottomNavigationLayoutTest.kt | 60 ------- .../java/com/m57/hermescontrol/Navigation.kt | 117 ------------- .../com/m57/hermescontrol/NavigationKeys.kt | 2 - .../data/config/ServerCollectionOps.kt | 9 - .../data/config/ServerStoreMigration.kt | 19 -- .../data/config/ServerStoreState.kt | 4 - .../hermescontrol/data/local/AuthManager.kt | 27 --- .../java/com/m57/hermescontrol/theme/Theme.kt | 3 - .../ui/settings/SettingsScreen.kt | 7 - .../ui/settings/SettingsSubPages.kt | 34 ---- .../ui/settings/SettingsViewModel.kt | 42 ----- .../settings/components/AppearanceSection.kt | 163 ------------------ app/src/main/res/values-ko/strings.xml | 11 -- app/src/main/res/values/strings.xml | 11 -- .../BottomNavigationSelectionTest.kt | 58 ------- .../data/config/ServerStoreTest.kt | 9 - .../ui/settings/SettingsViewModelTest.kt | 4 - 17 files changed, 580 deletions(-) delete mode 100644 app/src/androidTest/java/com/m57/hermescontrol/BottomNavigationLayoutTest.kt delete mode 100644 app/src/test/java/com/m57/hermescontrol/BottomNavigationSelectionTest.kt diff --git a/app/src/androidTest/java/com/m57/hermescontrol/BottomNavigationLayoutTest.kt b/app/src/androidTest/java/com/m57/hermescontrol/BottomNavigationLayoutTest.kt deleted file mode 100644 index 17ec867f..00000000 --- a/app/src/androidTest/java/com/m57/hermescontrol/BottomNavigationLayoutTest.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.m57.hermescontrol - -import androidx.compose.foundation.layout.Box -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.test.assertHeightIsAtLeast -import androidx.compose.ui.test.assertHeightIsEqualTo -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.unit.dp -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.MediumTest -import com.m57.hermescontrol.theme.BottomNavDisplayMode -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented tests for [Modifier.bottomNavigationHeight]. - * - * Each compact bottom-nav display mode maps to a specific bar height: - * - [BottomNavDisplayMode.ICON_ONLY] -> 56.dp (fixed) - * - [BottomNavDisplayMode.TEXT_ONLY] -> 44.dp (fixed) - * - [BottomNavDisplayMode.ICON_AND_TEXT] -> min 80.dp (can grow) - * - * We apply the modifier to a tagged [Box] and assert the resulting height on - * a real Compose host (this requires the instrumentation test runner, hence - * androidTest rather than the Robolectric-free JVM test source set). - */ -@RunWith(AndroidJUnit4::class) -@MediumTest -class BottomNavigationLayoutTest { - @get:Rule - val composeTestRule = createComposeRule() - - @Test - fun iconOnly_barHeightIs56dp() { - composeTestRule.setContent { - Box(Modifier.testTag("bar").bottomNavigationHeight(BottomNavDisplayMode.ICON_ONLY)) - } - composeTestRule.onNodeWithTag("bar").assertHeightIsEqualTo(56.dp) - } - - @Test - fun textOnly_barHeightIs44dp() { - composeTestRule.setContent { - Box(Modifier.testTag("bar").bottomNavigationHeight(BottomNavDisplayMode.TEXT_ONLY)) - } - composeTestRule.onNodeWithTag("bar").assertHeightIsEqualTo(44.dp) - } - - @Test - fun iconAndText_barHeightIsAtLeast80dp() { - composeTestRule.setContent { - Box(Modifier.testTag("bar").bottomNavigationHeight(BottomNavDisplayMode.ICON_AND_TEXT)) - } - // heightIn(min = 80.dp) — assert the measured height meets the minimum. - composeTestRule.onNodeWithTag("bar").assertHeightIsAtLeast(80.dp) - } -} diff --git a/app/src/main/java/com/m57/hermescontrol/Navigation.kt b/app/src/main/java/com/m57/hermescontrol/Navigation.kt index 1ff096a0..59098ff7 100644 --- a/app/src/main/java/com/m57/hermescontrol/Navigation.kt +++ b/app/src/main/java/com/m57/hermescontrol/Navigation.kt @@ -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 = - 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): List = - 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 { - SettingsNavBarPage( - onBack = { NavigationController.goBack() }, - viewModel = viewModel { SettingsViewModel() }, - ) - } entry { 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, diff --git a/app/src/main/java/com/m57/hermescontrol/NavigationKeys.kt b/app/src/main/java/com/m57/hermescontrol/NavigationKeys.kt index 624951bb..7472757f 100644 --- a/app/src/main/java/com/m57/hermescontrol/NavigationKeys.kt +++ b/app/src/main/java/com/m57/hermescontrol/NavigationKeys.kt @@ -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 diff --git a/app/src/main/java/com/m57/hermescontrol/data/config/ServerCollectionOps.kt b/app/src/main/java/com/m57/hermescontrol/data/config/ServerCollectionOps.kt index 856cd124..9083c240 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/config/ServerCollectionOps.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/config/ServerCollectionOps.kt @@ -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, ) } diff --git a/app/src/main/java/com/m57/hermescontrol/data/config/ServerStoreMigration.kt b/app/src/main/java/com/m57/hermescontrol/data/config/ServerStoreMigration.kt index a5a3d055..484dc100 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/config/ServerStoreMigration.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/config/ServerStoreMigration.kt @@ -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( diff --git a/app/src/main/java/com/m57/hermescontrol/data/config/ServerStoreState.kt b/app/src/main/java/com/m57/hermescontrol/data/config/ServerStoreState.kt index 6bfadc8d..5ca41990 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/config/ServerStoreState.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/config/ServerStoreState.kt @@ -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 = - listOf("ChatScreen", "SkillsScreen", "CronJobsScreen", "SystemScreen", "SettingsScreen"), val connectionProfiles: List = emptyList(), val selectedProfileId: String? = null, val pinnedModels: List = emptyList(), diff --git a/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt b/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt index 19b9c801..646cdf6d 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt @@ -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>(emptyList()) - val bottomNavItemsFlow: StateFlow> = _bottomNavItemsFlow.asStateFlow() - private val _themePreferenceFlow = MutableStateFlow(ThemePreference.SYSTEM) val themePreferenceFlow: StateFlow = _themePreferenceFlow.asStateFlow() @@ -73,9 +69,6 @@ object AuthManager { private val _themePresetFlow = MutableStateFlow(ThemePreset.DEFAULT) val themePresetFlow: StateFlow = _themePresetFlow.asStateFlow() - private val _bottomNavDisplayModeFlow = MutableStateFlow(BottomNavDisplayMode.ICON_AND_TEXT) - val bottomNavDisplayModeFlow: StateFlow = _bottomNavDisplayModeFlow.asStateFlow() - private val _tokenFlow = MutableStateFlow(null) val tokenFlow: StateFlow = _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 = serverStore.getLatestState().bottomNavItems - - /** Persist the ordered list of bottom-nav item keys (max 5, data-object names). */ - fun setBottomNavItems(items: List) { - 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) } - } } diff --git a/app/src/main/java/com/m57/hermescontrol/theme/Theme.kt b/app/src/main/java/com/m57/hermescontrol/theme/Theme.kt index ec9b9392..3dbc0d4d 100644 --- a/app/src/main/java/com/m57/hermescontrol/theme/Theme.kt +++ b/app/src/main/java/com/m57/hermescontrol/theme/Theme.kt @@ -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 } 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 b94a9fbf..f3221694 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 @@ -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), 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 337f986c..96ecaabe 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 @@ -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, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt index 700f60a5..33ef6173 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt @@ -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 = emptyList(), val typingEffectEnabled: Boolean = false, val typingEffectDelayMs: Int = 30, val profiles: List = 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> = - 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) { - _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() 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 0e1ce248..2b726d88 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 @@ -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, - availableNavItems: List>, - onReorderNavItems: (List) -> 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, - ) - } - } -} diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 53df2166..c74409ad 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -161,14 +161,6 @@ 지연: %1$d ms 10 ms 100 ms - 내비게이션 바 - 하단 바 항목 (%1$d/5) - 항목 추가 - 최대 5개 항목 - 하단 바 표시 모드 - 아이콘 + 텍스트 - 아이콘만 - 텍스트만 연결 테스트 정보 ⚡ 헤르메스 모바일 @@ -188,9 +180,6 @@ 프로필 삭제 토큰 숨기기 토큰 보기 - 위로 - 아래로 - %1$s 제거 시스템 라이트 다크 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 58c84eba..2d7e3199 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -175,14 +175,6 @@ Delay: %1$d ms 10 ms 100 ms - Navigation Bar - Bottom bar items (%1$d/5) - Add Item - Maximum of 5 items - Bottom Bar Display Mode - Icon & Text - Icons Only - Text Only Test Connection About ⚡ Hermes Mobile @@ -212,9 +204,6 @@ Delete Profile Hide token Show token - Move up - Move down - Remove %1$s System Light Dark diff --git a/app/src/test/java/com/m57/hermescontrol/BottomNavigationSelectionTest.kt b/app/src/test/java/com/m57/hermescontrol/BottomNavigationSelectionTest.kt deleted file mode 100644 index e46add54..00000000 --- a/app/src/test/java/com/m57/hermescontrol/BottomNavigationSelectionTest.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.m57.hermescontrol - -import androidx.navigation3.runtime.NavKey -import org.junit.Assert.assertEquals -import org.junit.Test - -/** - * Verifies that the bottom-nav selection key maps Settings drill-down - * destinations back to [SettingsScreen] so the indicator stays highlighted, - * while every other screen is returned unchanged. - */ -class BottomNavigationSelectionTest { - @Test - fun `SettingsScreen selects itself`() { - assertEquals(SettingsScreen, selectedBottomNavDestination(SettingsScreen)) - } - - @Test - fun `every Settings drill-down maps back to SettingsScreen`() { - val drillDowns: List = - listOf( - SettingsConnection, - SettingsAppearance, - SettingsChat, - SettingsNavBar, - SettingsBehavior, - SettingsAbout, - ) - for (destination in drillDowns) { - assertEquals( - "Expected $destination to map to SettingsScreen", - SettingsScreen, - selectedBottomNavDestination(destination), - ) - } - } - - @Test - fun `non-Settings screens are returned unchanged`() { - val others: List = - listOf( - ChatScreen, - CronJobsScreen, - WebhooksScreen, - GatewayScreen, - SkillsScreen, - LandingScreen, - AuthLoginScreen, - ) - for (screen in others) { - assertEquals( - "Expected $screen to be returned unchanged", - screen, - selectedBottomNavDestination(screen), - ) - } - } -} diff --git a/app/src/test/java/com/m57/hermescontrol/data/config/ServerStoreTest.kt b/app/src/test/java/com/m57/hermescontrol/data/config/ServerStoreTest.kt index 96fdb062..0bd06b2f 100644 --- a/app/src/test/java/com/m57/hermescontrol/data/config/ServerStoreTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/data/config/ServerStoreTest.kt @@ -68,15 +68,6 @@ class ServerStoreTest { ) val healed = state.selfHealed() assertNull(healed.selectedProfileId) - - // Test empty bottomNavItems - val state2 = - ServerStoreState( - bottomNavItems = emptyList(), - ) - val healed2 = state2.selfHealed() - assertEquals(5, healed2.bottomNavItems.size) - assertEquals("ChatScreen", healed2.bottomNavItems[0]) } @Test diff --git a/app/src/test/java/com/m57/hermescontrol/ui/settings/SettingsViewModelTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/settings/SettingsViewModelTest.kt index cae5fc6b..a88b5406 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/settings/SettingsViewModelTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/settings/SettingsViewModelTest.kt @@ -3,7 +3,6 @@ package com.m57.hermescontrol.ui.settings import com.m57.hermescontrol.data.config.ConnectionProfile import com.m57.hermescontrol.data.local.AuthManager import com.m57.hermescontrol.data.remote.ApiClient -import com.m57.hermescontrol.theme.BottomNavDisplayMode import com.m57.hermescontrol.theme.ThemePreference import com.m57.hermescontrol.theme.ThemePreset import io.mockk.every @@ -63,12 +62,10 @@ class SettingsViewModelTest { every { AuthManager.getThemePreference() } returns ThemePreference.SYSTEM every { AuthManager.isUseDynamicColors() } returns true every { AuthManager.getThemePreset() } returns ThemePreset.DEFAULT - every { AuthManager.getBottomNavItems() } returns emptyList() every { AuthManager.isTypingEffectEnabled() } returns true every { AuthManager.getTypingEffectDelayMs() } returns 30 every { AuthManager.getConnectionProfiles() } returns emptyList() every { AuthManager.getSelectedProfileId() } answers { storedSelectedProfileId } - every { AuthManager.getBottomNavDisplayMode() } returns BottomNavDisplayMode.ICON_AND_TEXT every { AuthManager.baseUrl() } returns "http://127.0.0.1:9119/" every { AuthManager.setHost(any()) } returns Unit every { AuthManager.setPort(any()) } returns Unit @@ -78,7 +75,6 @@ class SettingsViewModelTest { every { AuthManager.setThemePreference(any()) } returns Unit every { AuthManager.setUseDynamicColors(any()) } returns Unit every { AuthManager.setThemePreset(any()) } returns Unit - every { AuthManager.setBottomNavDisplayMode(any()) } returns Unit every { AuthManager.setTypingEffectEnabled(any()) } returns Unit every { AuthManager.setTypingEffectDelayMs(any()) } returns Unit every { AuthManager.setSelectedProfileId(any()) } answers {