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