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