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

@@ -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)
}
}
@@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@@ -22,9 +21,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer 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.NavigationDrawerItem
import androidx.compose.material3.NavigationDrawerItemDefaults import androidx.compose.material3.NavigationDrawerItemDefaults
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@@ -32,15 +28,12 @@ import androidx.compose.material3.Text
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp 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.local.AuthManager
import com.m57.hermescontrol.data.ws.ConnectionStatus import com.m57.hermescontrol.data.ws.ConnectionStatus
import com.m57.hermescontrol.data.ws.HermesWsClient import com.m57.hermescontrol.data.ws.HermesWsClient
import com.m57.hermescontrol.theme.BottomNavDisplayMode
import com.m57.hermescontrol.theme.LocalHermesStatusColors import com.m57.hermescontrol.theme.LocalHermesStatusColors
import com.m57.hermescontrol.ui.common.DisableDrawerGestures import com.m57.hermescontrol.ui.common.DisableDrawerGestures
import com.m57.hermescontrol.ui.common.DrawerGestureController 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.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.SettingsNavBarPage
import com.m57.hermescontrol.ui.settings.SettingsViewModel import com.m57.hermescontrol.ui.settings.SettingsViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.m57.hermescontrol.ui.authlogin.AuthLoginScreen as AuthLoginScreenContent import com.m57.hermescontrol.ui.authlogin.AuthLoginScreen as AuthLoginScreenContent
import com.m57.hermescontrol.ui.landing.LandingScreen as LandingScreenContent 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( private fun appEntryProvider(
sessionId: String?, sessionId: String?,
openDrawer: () -> Unit, openDrawer: () -> Unit,
@@ -159,12 +116,6 @@ private fun appEntryProvider(
viewModel = viewModel { SettingsViewModel() }, viewModel = viewModel { SettingsViewModel() },
) )
} }
entry<SettingsNavBar> {
SettingsNavBarPage(
onBack = { NavigationController.goBack() },
viewModel = viewModel { SettingsViewModel() },
)
}
entry<SettingsBehavior> { entry<SettingsBehavior> {
SettingsBehaviorPage( SettingsBehaviorPage(
onBack = { NavigationController.goBack() }, onBack = { NavigationController.goBack() },
@@ -189,7 +140,6 @@ fun MainNavigation(sessionId: String? = null) {
NavigationController.backStack = backStack NavigationController.backStack = backStack
val currentScreen = backStack.lastOrNull() ?: startScreen val currentScreen = backStack.lastOrNull() ?: startScreen
val selectedDestination = selectedBottomNavDestination(currentScreen)
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -204,19 +154,6 @@ fun MainNavigation(sessionId: String? = null) {
DrawerGestureController(drawerState, scope) 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() } } val openDrawer: () -> Unit = { scope.launch { drawerState.open() } }
CompositionLocalProvider(LocalDrawerGestureController provides gestureController) { CompositionLocalProvider(LocalDrawerGestureController provides gestureController) {
@@ -312,60 +249,6 @@ fun MainNavigation(sessionId: String? = null) {
) { ) {
Scaffold( Scaffold(
contentWindowInsets = WindowInsets.navigationBars, 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 -> ) { paddingValues ->
NavDisplay( NavDisplay(
backStack = backStack, backStack = backStack,
@@ -61,8 +61,6 @@ import kotlinx.serialization.Serializable
@Serializable data object SettingsChat : NavKey @Serializable data object SettingsChat : NavKey
@Serializable data object SettingsNavBar : NavKey
@Serializable data object SettingsBehavior : NavKey @Serializable data object SettingsBehavior : NavKey
@Serializable data object SettingsAbout : NavKey @Serializable data object SettingsAbout : NavKey
@@ -20,17 +20,8 @@ fun ServerStoreState.selfHealed(): ServerStoreState {
val hasActive = connectionProfiles.any { it.id == selectedProfileId } val hasActive = connectionProfiles.any { it.id == selectedProfileId }
val newSelected = if (selectedProfileId != null && !hasActive) null else 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( return copy(
selectedProfileId = newSelected, 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.model.PinnedModel
import com.m57.hermescontrol.data.remote.OkHttpProvider import com.m57.hermescontrol.data.remote.OkHttpProvider
import com.m57.hermescontrol.data.remote.ServerEndpoint import com.m57.hermescontrol.data.remote.ServerEndpoint
import com.m57.hermescontrol.theme.BottomNavDisplayMode
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 kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
@@ -27,8 +26,6 @@ class ServerStoreMigration(
"theme_preference", "theme_preference",
"use_dynamic_colors", "use_dynamic_colors",
"theme_preset", "theme_preset",
"bottom_nav_display_mode",
"bottom_nav_items",
"connection_profiles", "connection_profiles",
"selected_profile_id", "selected_profile_id",
"pinned_models", "pinned_models",
@@ -65,20 +62,6 @@ class ServerStoreMigration(
runCatching { ThemePreset.valueOf(themePresetString!!) } runCatching { ThemePreset.valueOf(themePresetString!!) }
.getOrDefault(ThemePreset.DEFAULT) .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 connectionProfilesRaw = prefs.getString("connection_profiles", null)
val connectionProfiles = val connectionProfiles =
if (connectionProfilesRaw != null) { if (connectionProfilesRaw != null) {
@@ -117,8 +100,6 @@ class ServerStoreMigration(
themePreference = themePreference, themePreference = themePreference,
useDynamicColors = useDynamicColors, useDynamicColors = useDynamicColors,
themePreset = themePreset, themePreset = themePreset,
bottomNavDisplayMode = bottomNavDisplayMode,
bottomNavItems = bottomNavItems,
connectionProfiles = connectionProfiles =
connectionProfiles.map { profile -> connectionProfiles.map { profile ->
profile.copy( profile.copy(
@@ -1,7 +1,6 @@
package com.m57.hermescontrol.data.config package com.m57.hermescontrol.data.config
import com.m57.hermescontrol.data.model.PinnedModel import com.m57.hermescontrol.data.model.PinnedModel
import com.m57.hermescontrol.theme.BottomNavDisplayMode
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 kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -16,9 +15,6 @@ data class ServerStoreState(
val themePreference: ThemePreference = ThemePreference.SYSTEM, val themePreference: ThemePreference = ThemePreference.SYSTEM,
val useDynamicColors: Boolean = true, val useDynamicColors: Boolean = true,
val themePreset: ThemePreset = ThemePreset.DEFAULT, 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 connectionProfiles: List<ConnectionProfile> = emptyList(),
val selectedProfileId: String? = null, val selectedProfileId: String? = null,
val pinnedModels: List<PinnedModel> = emptyList(), 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.CleartextPolicy
import com.m57.hermescontrol.data.remote.CookieManager import com.m57.hermescontrol.data.remote.CookieManager
import com.m57.hermescontrol.data.remote.ServerEndpoint import com.m57.hermescontrol.data.remote.ServerEndpoint
import com.m57.hermescontrol.theme.BottomNavDisplayMode
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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -61,9 +60,6 @@ object AuthManager {
"AuthManager not initialized. Call init(context) first.", "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) private val _themePreferenceFlow = MutableStateFlow<ThemePreference>(ThemePreference.SYSTEM)
val themePreferenceFlow: StateFlow<ThemePreference> = _themePreferenceFlow.asStateFlow() val themePreferenceFlow: StateFlow<ThemePreference> = _themePreferenceFlow.asStateFlow()
@@ -73,9 +69,6 @@ object AuthManager {
private val _themePresetFlow = MutableStateFlow<ThemePreset>(ThemePreset.DEFAULT) private val _themePresetFlow = MutableStateFlow<ThemePreset>(ThemePreset.DEFAULT)
val themePresetFlow: StateFlow<ThemePreset> = _themePresetFlow.asStateFlow() 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) private val _tokenFlow = MutableStateFlow<String?>(null)
val tokenFlow: StateFlow<String?> = _tokenFlow.asStateFlow() val tokenFlow: StateFlow<String?> = _tokenFlow.asStateFlow()
@@ -133,11 +126,9 @@ object AuthManager {
scope.launch { scope.launch {
store.stateFlow.collect { state -> store.stateFlow.collect { state ->
_bottomNavItemsFlow.value = state.bottomNavItems
_themePreferenceFlow.value = state.themePreference _themePreferenceFlow.value = state.themePreference
_useDynamicColorsFlow.value = state.useDynamicColors _useDynamicColorsFlow.value = state.useDynamicColors
_themePresetFlow.value = state.themePreset _themePresetFlow.value = state.themePreset
_bottomNavDisplayModeFlow.value = state.bottomNavDisplayMode
// B7 (Jul 08 2026, kanban t_470): keep cookie scope aligned with active profile. // B7 (Jul 08 2026, kanban t_470): keep cookie scope aligned with active profile.
syncCookieStoreForProfile(state.selectedProfileId) 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 ─────────────────────────────────────────────────── // ── Typing Effect ───────────────────────────────────────────────────
fun isTypingEffectEnabled(): Boolean = serverStore.getLatestState().typingEffectEnabled fun isTypingEffectEnabled(): Boolean = serverStore.getLatestState().typingEffectEnabled
@@ -544,12 +525,4 @@ object AuthManager {
fun setTypingEffectDelayMs(delayMs: Int) { fun setTypingEffectDelayMs(delayMs: Int) {
serverStore.update { it.copy(typingEffectDelayMs = delayMs) } 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 @Serializable
enum class ThemePreset { DEFAULT, MONOCHROME, GRUVBOX, CATPPUCCIN, AMOLED, NEON_NOIR } 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 LocalThemePreference = compositionLocalOf { ThemePreference.SYSTEM }
val LocalThemePreset = compositionLocalOf { ThemePreset.DEFAULT } 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.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.Dashboard
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info
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
@@ -40,7 +39,6 @@ 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.SettingsNavBar
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
@@ -100,11 +98,6 @@ fun SettingsScreen(
}, },
onClick = { NavigationController.navigateTo(SettingsChat) }, onClick = { NavigationController.navigateTo(SettingsChat) },
), ),
SettingsRow(
icon = Icons.Filled.Dashboard,
label = stringResource(R.string.settings_sec_nav_bar),
onClick = { NavigationController.navigateTo(SettingsNavBar) },
),
SettingsRow( SettingsRow(
icon = Icons.Filled.Tune, icon = Icons.Filled.Tune,
label = stringResource(R.string.settings_sec_behavior), 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.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.NavBarSection
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
@@ -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 @Composable
internal fun SettingsBehaviorPage( internal fun SettingsBehaviorPage(
onBack: () -> Unit, onBack: () -> Unit,
@@ -2,7 +2,6 @@ package com.m57.hermescontrol.ui.settings
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.m57.hermescontrol.ScreenRegistry
import com.m57.hermescontrol.data.config.ConnectionProfile import com.m57.hermescontrol.data.config.ConnectionProfile
import com.m57.hermescontrol.data.config.resolveBaseUrl import com.m57.hermescontrol.data.config.resolveBaseUrl
import com.m57.hermescontrol.data.local.AuthManager 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.ServerEndpoint
import com.m57.hermescontrol.data.remote.safeApiCall import com.m57.hermescontrol.data.remote.safeApiCall
import com.m57.hermescontrol.data.ws.HermesWsClient import com.m57.hermescontrol.data.ws.HermesWsClient
import com.m57.hermescontrol.theme.BottomNavDisplayMode
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 kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
@@ -35,13 +33,11 @@ data class SettingsUiState(
val isTesting: Boolean = false, val isTesting: Boolean = false,
val testResult: String? = null, val testResult: String? = null,
val isSaved: Boolean = false, val isSaved: Boolean = false,
val selectedNavItems: List<String> = emptyList(),
val typingEffectEnabled: Boolean = false, val typingEffectEnabled: Boolean = false,
val typingEffectDelayMs: Int = 30, val typingEffectDelayMs: Int = 30,
val profiles: List<ConnectionProfile> = emptyList(), val profiles: List<ConnectionProfile> = emptyList(),
val selectedProfileId: String? = null, val selectedProfileId: String? = null,
val renameProfileName: String = "", val renameProfileName: String = "",
val bottomNavDisplayMode: BottomNavDisplayMode = BottomNavDisplayMode.ICON_AND_TEXT,
val appLanguage: String = "system", val appLanguage: String = "system",
// Profile add/edit dialog // Profile add/edit dialog
val showProfileDialog: Boolean = false, val showProfileDialog: Boolean = false,
@@ -77,11 +73,9 @@ class SettingsViewModel(
val themePreference = AuthManager.getThemePreference() val themePreference = AuthManager.getThemePreference()
val useDynamicColors = AuthManager.isUseDynamicColors() val useDynamicColors = AuthManager.isUseDynamicColors()
val themePreset = AuthManager.getThemePreset() val themePreset = AuthManager.getThemePreset()
val selectedNavItems = AuthManager.getBottomNavItems()
val typingEffectEnabled = AuthManager.isTypingEffectEnabled() val typingEffectEnabled = AuthManager.isTypingEffectEnabled()
val typingEffectDelayMs = AuthManager.getTypingEffectDelayMs() val typingEffectDelayMs = AuthManager.getTypingEffectDelayMs()
val profiles = AuthManager.getConnectionProfiles() val profiles = AuthManager.getConnectionProfiles()
val bottomNavDisplayMode = AuthManager.getBottomNavDisplayMode()
val appLanguage = AuthManager.getAppLanguage() val appLanguage = AuthManager.getAppLanguage()
val renameProfileName = val renameProfileName =
profiles.firstOrNull { p -> p.id == selectedId }?.name ?: "" profiles.firstOrNull { p -> p.id == selectedId }?.name ?: ""
@@ -98,13 +92,11 @@ class SettingsViewModel(
themePreference = themePreference, themePreference = themePreference,
useDynamicColors = useDynamicColors, useDynamicColors = useDynamicColors,
themePreset = themePreset, themePreset = themePreset,
selectedNavItems = selectedNavItems,
typingEffectEnabled = typingEffectEnabled, typingEffectEnabled = typingEffectEnabled,
typingEffectDelayMs = typingEffectDelayMs, typingEffectDelayMs = typingEffectDelayMs,
profiles = profiles, profiles = profiles,
selectedProfileId = selectedId, selectedProfileId = selectedId,
renameProfileName = renameProfileName, renameProfileName = renameProfileName,
bottomNavDisplayMode = bottomNavDisplayMode,
appLanguage = appLanguage, appLanguage = appLanguage,
) )
} }
@@ -312,11 +304,6 @@ class SettingsViewModel(
AuthManager.setAppLanguage(code) AuthManager.setAppLanguage(code)
} }
fun onBottomNavDisplayModeChange(mode: BottomNavDisplayMode) {
_uiState.update { it.copy(bottomNavDisplayMode = mode, isSaved = false) }
AuthManager.setBottomNavDisplayMode(mode)
}
fun onTypingEffectEnabledChange(enabled: Boolean) { fun onTypingEffectEnabledChange(enabled: Boolean) {
_uiState.update { it.copy(typingEffectEnabled = enabled, isSaved = false) } _uiState.update { it.copy(typingEffectEnabled = enabled, isSaved = false) }
AuthManager.setTypingEffectEnabled(enabled) AuthManager.setTypingEffectEnabled(enabled)
@@ -336,34 +323,6 @@ class SettingsViewModel(
// Don't rebuild ApiClient here — let the navigation complete first // 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() { fun save() {
val state = _uiState.value val state = _uiState.value
val normalized = val normalized =
@@ -379,7 +338,6 @@ class SettingsViewModel(
AuthManager.setThemePreference(state.themePreference) AuthManager.setThemePreference(state.themePreference)
AuthManager.setUseDynamicColors(state.useDynamicColors) AuthManager.setUseDynamicColors(state.useDynamicColors)
AuthManager.setThemePreset(state.themePreset) AuthManager.setThemePreset(state.themePreset)
AuthManager.setBottomNavDisplayMode(state.bottomNavDisplayMode)
AuthManager.setTypingEffectEnabled(state.typingEffectEnabled) AuthManager.setTypingEffectEnabled(state.typingEffectEnabled)
AuthManager.setTypingEffectDelayMs(state.typingEffectDelayMs) AuthManager.setTypingEffectDelayMs(state.typingEffectDelayMs)
ApiClient.rebuild() ApiClient.rebuild()
@@ -11,16 +11,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size 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.DropdownMenu
import androidx.compose.material3.DropdownMenuItem 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.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButton
@@ -40,7 +32,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.m57.hermescontrol.R import com.m57.hermescontrol.R
import com.m57.hermescontrol.theme.BottomNavDisplayMode
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
@@ -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,
)
}
}
}
-11
View File
@@ -161,14 +161,6 @@
<string name="settings_item_typing_delay">지연: %1$d ms</string> <string name="settings_item_typing_delay">지연: %1$d ms</string>
<string name="settings_delay_10ms">10 ms</string> <string name="settings_delay_10ms">10 ms</string>
<string name="settings_delay_100ms">100 ms</string> <string name="settings_delay_100ms">100 ms</string>
<string name="settings_sec_nav_bar">내비게이션 바</string>
<string name="settings_item_nav_items">하단 바 항목 (%1$d/5)</string>
<string name="settings_action_add_item">항목 추가</string>
<string name="settings_nav_max_reached">최대 5개 항목</string>
<string name="settings_item_bottom_nav_display_mode">하단 바 표시 모드</string>
<string name="settings_display_mode_icon_and_text">아이콘 + 텍스트</string>
<string name="settings_display_mode_icon_only">아이콘만</string>
<string name="settings_display_mode_text_only">텍스트만</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_about_app_name">⚡ 헤르메스 모바일</string> <string name="settings_about_app_name">⚡ 헤르메스 모바일</string>
@@ -188,9 +180,6 @@
<string name="content_desc_delete_profile">프로필 삭제</string> <string name="content_desc_delete_profile">프로필 삭제</string>
<string name="content_desc_hide_token">토큰 숨기기</string> <string name="content_desc_hide_token">토큰 숨기기</string>
<string name="content_desc_show_token">토큰 보기</string> <string name="content_desc_show_token">토큰 보기</string>
<string name="content_desc_move_up">위로</string>
<string name="content_desc_move_down">아래로</string>
<string name="content_desc_remove_item">%1$s 제거</string>
<string name="theme_system">시스템</string> <string name="theme_system">시스템</string>
<string name="theme_light">라이트</string> <string name="theme_light">라이트</string>
<string name="theme_dark">다크</string> <string name="theme_dark">다크</string>
-11
View File
@@ -175,14 +175,6 @@
<string name="settings_item_typing_delay">Delay: %1$d ms</string> <string name="settings_item_typing_delay">Delay: %1$d ms</string>
<string name="settings_delay_10ms">10 ms</string> <string name="settings_delay_10ms">10 ms</string>
<string name="settings_delay_100ms">100 ms</string> <string name="settings_delay_100ms">100 ms</string>
<string name="settings_sec_nav_bar">Navigation Bar</string>
<string name="settings_item_nav_items">Bottom bar items (%1$d/5)</string>
<string name="settings_action_add_item">Add Item</string>
<string name="settings_nav_max_reached">Maximum of 5 items</string>
<string name="settings_item_bottom_nav_display_mode">Bottom Bar Display Mode</string>
<string name="settings_display_mode_icon_and_text">Icon &amp; Text</string>
<string name="settings_display_mode_icon_only">Icons Only</string>
<string name="settings_display_mode_text_only">Text Only</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_about_app_name">⚡ Hermes Mobile</string> <string name="settings_about_app_name">⚡ Hermes Mobile</string>
@@ -212,9 +204,6 @@
<string name="content_desc_delete_profile">Delete Profile</string> <string name="content_desc_delete_profile">Delete Profile</string>
<string name="content_desc_hide_token">Hide token</string> <string name="content_desc_hide_token">Hide token</string>
<string name="content_desc_show_token">Show token</string> <string name="content_desc_show_token">Show token</string>
<string name="content_desc_move_up">Move up</string>
<string name="content_desc_move_down">Move down</string>
<string name="content_desc_remove_item">Remove %1$s</string>
<string name="theme_system">System</string> <string name="theme_system">System</string>
<string name="theme_light">Light</string> <string name="theme_light">Light</string>
<string name="theme_dark">Dark</string> <string name="theme_dark">Dark</string>
@@ -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<NavKey> =
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<NavKey> =
listOf(
ChatScreen,
CronJobsScreen,
WebhooksScreen,
GatewayScreen,
SkillsScreen,
LandingScreen,
AuthLoginScreen,
)
for (screen in others) {
assertEquals(
"Expected $screen to be returned unchanged",
screen,
selectedBottomNavDestination(screen),
)
}
}
}
@@ -68,15 +68,6 @@ class ServerStoreTest {
) )
val healed = state.selfHealed() val healed = state.selfHealed()
assertNull(healed.selectedProfileId) 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 @Test
@@ -3,7 +3,6 @@ package com.m57.hermescontrol.ui.settings
import com.m57.hermescontrol.data.config.ConnectionProfile import com.m57.hermescontrol.data.config.ConnectionProfile
import com.m57.hermescontrol.data.local.AuthManager import com.m57.hermescontrol.data.local.AuthManager
import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.ApiClient
import com.m57.hermescontrol.theme.BottomNavDisplayMode
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 io.mockk.every import io.mockk.every
@@ -63,12 +62,10 @@ class SettingsViewModelTest {
every { AuthManager.getThemePreference() } returns ThemePreference.SYSTEM every { AuthManager.getThemePreference() } returns ThemePreference.SYSTEM
every { AuthManager.isUseDynamicColors() } returns true every { AuthManager.isUseDynamicColors() } returns true
every { AuthManager.getThemePreset() } returns ThemePreset.DEFAULT every { AuthManager.getThemePreset() } returns ThemePreset.DEFAULT
every { AuthManager.getBottomNavItems() } returns emptyList()
every { AuthManager.isTypingEffectEnabled() } returns true every { AuthManager.isTypingEffectEnabled() } returns true
every { AuthManager.getTypingEffectDelayMs() } returns 30 every { AuthManager.getTypingEffectDelayMs() } returns 30
every { AuthManager.getConnectionProfiles() } returns emptyList() every { AuthManager.getConnectionProfiles() } returns emptyList()
every { AuthManager.getSelectedProfileId() } answers { storedSelectedProfileId } 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.baseUrl() } returns "http://127.0.0.1:9119/"
every { AuthManager.setHost(any()) } returns Unit every { AuthManager.setHost(any()) } returns Unit
every { AuthManager.setPort(any()) } returns Unit every { AuthManager.setPort(any()) } returns Unit
@@ -78,7 +75,6 @@ class SettingsViewModelTest {
every { AuthManager.setThemePreference(any()) } returns Unit every { AuthManager.setThemePreference(any()) } returns Unit
every { AuthManager.setUseDynamicColors(any()) } returns Unit every { AuthManager.setUseDynamicColors(any()) } returns Unit
every { AuthManager.setThemePreset(any()) } returns Unit every { AuthManager.setThemePreset(any()) } returns Unit
every { AuthManager.setBottomNavDisplayMode(any()) } returns Unit
every { AuthManager.setTypingEffectEnabled(any()) } returns Unit every { AuthManager.setTypingEffectEnabled(any()) } returns Unit
every { AuthManager.setTypingEffectDelayMs(any()) } returns Unit every { AuthManager.setTypingEffectDelayMs(any()) } returns Unit
every { AuthManager.setSelectedProfileId(any()) } answers { every { AuthManager.setSelectedProfileId(any()) } answers {