diff --git a/app/src/main/java/com/m57/hermescontrol/Navigation.kt b/app/src/main/java/com/m57/hermescontrol/Navigation.kt index a5ec8221..0e1cde44 100644 --- a/app/src/main/java/com/m57/hermescontrol/Navigation.kt +++ b/app/src/main/java/com/m57/hermescontrol/Navigation.kt @@ -44,6 +44,7 @@ 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 @@ -70,6 +71,7 @@ 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 kotlinx.coroutines.launch import com.m57.hermescontrol.ui.achievements.AchievementsScreen as AchievementsScreenContent import com.m57.hermescontrol.ui.channels.ChannelsScreen as ChannelsScreenContent @@ -164,7 +166,6 @@ private val DRAWER_ENTRIES = DrawerEntry(GatewayScreen, R.string.screen_gateway, Icons.Filled.Bolt, DrawerSection.AUTOMATE), // Configure DrawerEntry(SkillsScreen, R.string.screen_skills, Icons.Filled.Extension, DrawerSection.CONFIGURE), - DrawerEntry(SettingsScreen, R.string.screen_settings, Icons.Filled.Settings, DrawerSection.CONFIGURE), DrawerEntry(ToolsetsScreen, R.string.screen_toolsets, Icons.Filled.Build, DrawerSection.CONFIGURE), DrawerEntry(PluginsScreen, R.string.screen_plugins, Icons.Filled.Memory, DrawerSection.CONFIGURE), DrawerEntry(ConfigScreen, R.string.screen_config, Icons.Filled.Code, DrawerSection.CONFIGURE), @@ -183,6 +184,7 @@ private val DRAWER_ENTRIES = DrawerEntry(LogsScreen, R.string.screen_logs, Icons.Filled.HistoryEdu, DrawerSection.INSPECT), DrawerEntry(KanbanScreen, R.string.screen_kanban, Icons.Filled.Dashboard, DrawerSection.INSPECT), DrawerEntry(AchievementsScreen, R.string.screen_achievements, Icons.Filled.Info, DrawerSection.INSPECT), + DrawerEntry(SettingsScreen, R.string.screen_settings, Icons.Filled.Settings, DrawerSection.INSPECT), ) private val DRAWER_GESTURE_SCREENS: Set = ALL_NAV_ITEMS.mapTo(mutableSetOf()) { it.key } @@ -339,6 +341,7 @@ fun MainNavigation(sessionId: String? = null) { // customises items in Settings. Using collectAsState() ensures the NavigationBar // recomposes and reflects choices instantly. 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 } } @@ -375,7 +378,7 @@ fun MainNavigation(sessionId: String? = null) { } Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(start = 20.dp, top = 20.dp, end = 16.dp, bottom = 4.dp), + modifier = Modifier.padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 2.dp), ) { Text( text = stringResource(R.string.nav_drawer_title), @@ -395,7 +398,7 @@ fun MainNavigation(sessionId: String? = null) { } Text( text = stringResource(R.string.nav_drawer_subtitle), - modifier = Modifier.padding(start = 20.dp, bottom = 12.dp, end = 16.dp), + modifier = Modifier.padding(start = 16.dp, bottom = 8.dp, end = 16.dp), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -406,9 +409,9 @@ fun MainNavigation(sessionId: String? = null) { text = stringResource(section.titleRes).uppercase(), modifier = Modifier.padding( - start = 20.dp, - top = 16.dp, - bottom = 6.dp, + start = 16.dp, + top = 8.dp, + bottom = 4.dp, end = 16.dp, ), style = MaterialTheme.typography.labelSmall, @@ -434,7 +437,7 @@ fun MainNavigation(sessionId: String? = null) { ), modifier = Modifier - .padding(horizontal = 12.dp, vertical = 2.dp) + .padding(horizontal = 8.dp, vertical = 1.dp) .testTag( "drawer_${entry.key::class.simpleName?.lowercase()?.removeSuffix( "screen", @@ -455,13 +458,53 @@ fun MainNavigation(sessionId: String? = null) { contentWindowInsets = WindowInsets.navigationBars, bottomBar = { if (showBottomBar) { - NavigationBar { + val barHeight = + when (bottomNavDisplayMode) { + BottomNavDisplayMode.ICON_ONLY -> 56.dp + BottomNavDisplayMode.TEXT_ONLY -> 44.dp + BottomNavDisplayMode.ICON_AND_TEXT -> 80.dp + } + NavigationBar( + modifier = Modifier.height(barHeight), + ) { 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 = currentScreen == item.key + NavigationBarItem( - selected = currentScreen == item.key, + selected = isSelected, onClick = { NavigationController.navigateTo(item.key) }, - icon = { Icon(item.icon, contentDescription = stringResource(item.labelRes)) }, - label = { Text(stringResource(item.labelRes)) }, + 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") ?: ""}", diff --git a/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt b/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt index 4564a834..78deba70 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.SharedPreferences import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey +import com.m57.hermescontrol.theme.BottomNavDisplayMode import com.m57.hermescontrol.theme.ThemePreference import com.m57.hermescontrol.theme.ThemePreset import kotlinx.coroutines.flow.MutableStateFlow @@ -31,6 +32,7 @@ object AuthManager { private const val KEY_SELECTED_PROFILE_ID = "selected_profile_id" private const val KEY_USE_DYNAMIC_COLORS = "use_dynamic_colors" private const val KEY_THEME_PRESET = "theme_preset" + private const val KEY_BOTTOM_NAV_DISPLAY_MODE = "bottom_nav_display_mode" private const val DEFAULT_HOST = "127.0.0.1" private const val DEFAULT_PORT = 9119 @@ -52,6 +54,9 @@ object AuthManager { private val _themePresetFlow = MutableStateFlow(ThemePreset.DEFAULT) val themePresetFlow: StateFlow = _themePresetFlow.asStateFlow() + + private val _bottomNavDisplayModeFlow = MutableStateFlow(BottomNavDisplayMode.ICON_AND_TEXT) + val bottomNavDisplayModeFlow: StateFlow = _bottomNavDisplayModeFlow.asStateFlow() private val gson = com.google.gson.Gson() /** @@ -81,6 +86,7 @@ object AuthManager { _themePreferenceFlow.value = getThemePreference() _useDynamicColorsFlow.value = isUseDynamicColors() _themePresetFlow.value = getThemePreset() + _bottomNavDisplayModeFlow.value = getBottomNavDisplayMode() } } @@ -282,4 +288,16 @@ object AuthManager { fun setTypingEffectDelayMs(delayMs: Int) { requirePrefs().edit().putInt(KEY_TYPING_EFFECT_DELAY_MS, delayMs).apply() } + + // ── Bottom Nav Display Mode ────────────────────────────────────────── + + fun getBottomNavDisplayMode(): BottomNavDisplayMode = + requirePrefs().getString(KEY_BOTTOM_NAV_DISPLAY_MODE, BottomNavDisplayMode.ICON_AND_TEXT.name)?.let { name -> + runCatching { BottomNavDisplayMode.valueOf(name) }.getOrNull() + } ?: BottomNavDisplayMode.ICON_AND_TEXT + + fun setBottomNavDisplayMode(mode: BottomNavDisplayMode) { + requirePrefs().edit().putString(KEY_BOTTOM_NAV_DISPLAY_MODE, mode.name).apply() + _bottomNavDisplayModeFlow.value = mode + } } diff --git a/app/src/main/java/com/m57/hermescontrol/theme/Spacing.kt b/app/src/main/java/com/m57/hermescontrol/theme/Spacing.kt index 1cda0b4e..8faccda8 100644 --- a/app/src/main/java/com/m57/hermescontrol/theme/Spacing.kt +++ b/app/src/main/java/com/m57/hermescontrol/theme/Spacing.kt @@ -18,12 +18,12 @@ import androidx.compose.ui.unit.dp * Access in composables via `LocalSpacing.current` or the [Spacing] object. */ object Spacing { - val xs = 4.dp - val sm = 8.dp - val md = 16.dp - val lg = 24.dp - val xl = 32.dp - val xxl = 48.dp + val xs = 2.dp + val sm = 6.dp + val md = 12.dp + val lg = 18.dp + val xl = 24.dp + val xxl = 36.dp } /** Alias for backwards-compatibility — existing `Spacing.xs` calls still work. */ diff --git a/app/src/main/java/com/m57/hermescontrol/theme/Theme.kt b/app/src/main/java/com/m57/hermescontrol/theme/Theme.kt index 6adf38ea..57ebd37d 100644 --- a/app/src/main/java/com/m57/hermescontrol/theme/Theme.kt +++ b/app/src/main/java/com/m57/hermescontrol/theme/Theme.kt @@ -17,6 +17,8 @@ enum class ThemePreference { SYSTEM, LIGHT, DARK } enum class ThemePreset { DEFAULT, MONOCHROME, GRUVBOX, CATPPUCCIN, AMOLED } +enum class BottomNavDisplayMode { ICON_AND_TEXT, ICON_ONLY, TEXT_ONLY } + val LocalThemePreference = compositionLocalOf { ThemePreference.SYSTEM } val LocalThemePreset = compositionLocalOf { ThemePreset.DEFAULT } @@ -242,8 +244,14 @@ fun HermesControlTheme( val dynamicAvailable = useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S val colorScheme = when { - dynamicAvailable && darkTheme -> dynamicDarkColorScheme(context) - dynamicAvailable && !darkTheme -> dynamicLightColorScheme(context) + dynamicAvailable && darkTheme -> { + dynamicDarkColorScheme(context) + } + + dynamicAvailable && !darkTheme -> { + dynamicLightColorScheme(context) + } + else -> { when (themePreset) { ThemePreset.DEFAULT -> if (darkTheme) HermesDarkColorScheme else HermesLightColorScheme @@ -269,6 +277,7 @@ fun HermesControlTheme( infoContainer = Color(0xFF3C3836), ) } + themePreset == ThemePreset.GRUVBOX && !darkTheme -> { HermesStatusColors( success = Color(0xFF98971A), @@ -281,6 +290,7 @@ fun HermesControlTheme( infoContainer = Color(0xFFEBDBB2), ) } + themePreset == ThemePreset.CATPPUCCIN && darkTheme -> { HermesStatusColors( success = Color(0xFFA6E3A1), @@ -293,6 +303,7 @@ fun HermesControlTheme( infoContainer = Color(0xFF313244), ) } + themePreset == ThemePreset.CATPPUCCIN && !darkTheme -> { HermesStatusColors( success = Color(0xFF40A02B), @@ -305,6 +316,7 @@ fun HermesControlTheme( infoContainer = Color(0xFFE6E9EF), ) } + darkTheme -> { HermesStatusColors( success = StatusGreen, @@ -317,6 +329,7 @@ fun HermesControlTheme( infoContainer = StatusBlueContainer, ) } + else -> { HermesStatusColors( success = Color(0xFF1B873A), diff --git a/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsScreen.kt index 95a83025..56be1cda 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsScreen.kt @@ -21,6 +21,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -33,6 +36,7 @@ import com.m57.hermescontrol.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState +import com.m57.hermescontrol.ui.common.SearchBar @Composable fun AchievementsScreen( @@ -41,6 +45,16 @@ fun AchievementsScreen( viewModel: AchievementsViewModel = viewModel { AchievementsViewModel() }, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() + var query by remember { mutableStateOf("") } + + val filteredAchievements = + remember(query, state.achievements) { + state.achievements.filter { achievement -> + achievement.name.contains(query, ignoreCase = true) || + achievement.description?.contains(query, ignoreCase = true) == true || + achievement.category?.contains(query, ignoreCase = true) == true + } + } LaunchedEffect(Unit) { viewModel.loadAchievements() @@ -80,7 +94,14 @@ fun AchievementsScreen( contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { - items(state.achievements) { achievement -> + item { + SearchBar( + query = query, + onQueryChange = { query = it }, + placeholder = "Search achievements...", + ) + } + items(filteredAchievements) { achievement -> val pct = achievement.progress_pct?.toFloat()?.div(100f) ?: 0f Card( diff --git a/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt index f5ff2403..8d3f55d3 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt @@ -3,7 +3,6 @@ package com.m57.hermescontrol.ui.channels import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -39,6 +38,9 @@ import com.m57.hermescontrol.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState +import com.m57.hermescontrol.ui.common.SearchBar +import com.m57.hermescontrol.ui.common.listContentPadding +import com.m57.hermescontrol.ui.common.listItemSpacing @Composable fun ChannelsScreen( @@ -49,6 +51,15 @@ fun ChannelsScreen( val state by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current + var query by remember { mutableStateOf("") } + + val filteredPlatforms = + remember(query, state.platforms) { + state.platforms.filter { platform -> + platform.name.contains(query, ignoreCase = true) + } + } + LaunchedEffect(Unit) { viewModel.loadPlatforms() } @@ -90,10 +101,17 @@ fun ChannelsScreen( else -> { LazyColumn( modifier = Modifier.fillMaxSize().padding(paddingValues), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = listContentPadding, + verticalArrangement = listItemSpacing, ) { - items(state.platforms, key = { it.id }) { platform -> + item { + SearchBar( + query = query, + onQueryChange = { query = it }, + placeholder = "Search channels...", + ) + } + items(filteredPlatforms, key = { it.id }) { platform -> var showConfigureForm by remember { mutableStateOf(false) } Card(modifier = Modifier.fillMaxWidth()) { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt index eb7117a2..78fecff1 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt @@ -157,7 +157,7 @@ private fun UserBubble( modifier = modifier .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 3.dp), + .padding(horizontal = 8.dp, vertical = 2.dp), contentAlignment = Alignment.CenterEnd, ) { val gradientBrush = @@ -208,7 +208,7 @@ private fun UserBubble( color = Color.Transparent, tonalElevation = 0.dp, ) { - Column(modifier = Modifier.padding(12.dp)) { + Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)) { SelectionContainer { Text( text = highlightedText, @@ -304,7 +304,7 @@ private fun AssistantBubble( modifier = modifier .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 3.dp), + .padding(horizontal = 8.dp, vertical = 2.dp), contentAlignment = Alignment.CenterStart, ) { Box { @@ -335,7 +335,7 @@ private fun AssistantBubble( ), tonalElevation = 1.dp, ) { - Column(modifier = Modifier.padding(12.dp)) { + Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)) { SelectionContainer { RichText( text = message.content, @@ -402,7 +402,7 @@ private fun SystemBubble( modifier = modifier .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 6.dp), + .padding(horizontal = 16.dp, vertical = 4.dp), contentAlignment = Alignment.Center, ) { Text( @@ -612,7 +612,7 @@ private fun ToolBubble( modifier = modifier .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 2.dp), + .padding(horizontal = 8.dp, vertical = 1.dp), contentAlignment = Alignment.CenterStart, ) { Card( @@ -624,7 +624,7 @@ private fun ToolBubble( modifier = Modifier .animateContentSize() - .padding(horizontal = 12.dp, vertical = 8.dp), + .padding(horizontal = 10.dp, vertical = 6.dp), ) { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/common/StateViews.kt b/app/src/main/java/com/m57/hermescontrol/ui/common/StateViews.kt index 1c310c83..31e32f2b 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/common/StateViews.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/common/StateViews.kt @@ -157,6 +157,6 @@ fun EmptyState( // ── Padding conventions ────────────────────────────────────────────── -val listContentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp) +val listContentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp) -val listItemSpacing = Arrangement.spacedBy(8.dp) +val listItemSpacing = Arrangement.spacedBy(6.dp) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt index da81522b..e6aff9bc 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt @@ -49,6 +49,7 @@ import com.m57.hermescontrol.data.model.KanbanTask import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState +import com.m57.hermescontrol.ui.common.SearchBar private const val DEFAULT_COLUMN = "todo" @@ -61,6 +62,18 @@ fun KanbanScreen( ) { val state by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current + + var query by remember { mutableStateOf("") } + + val filteredTasks = + remember(query, state.tasks) { + state.tasks.filter { task -> + task.title.contains(query, ignoreCase = true) || + task.description?.contains(query, ignoreCase = true) == true || + task.status.contains(query, ignoreCase = true) || + task.assignedTo?.contains(query, ignoreCase = true) == true + } + } var showAddTaskDialog by remember { mutableStateOf(false) } LaunchedEffect(Unit) { @@ -107,6 +120,12 @@ fun KanbanScreen( Column( modifier = Modifier.fillMaxSize().padding(paddingValues), ) { + SearchBar( + query = query, + onQueryChange = { query = it }, + placeholder = "Filter tasks by title, status, or assignee...", + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) // Board selector tab row if (state.boards.isNotEmpty()) { PrimaryScrollableTabRow( @@ -140,7 +159,7 @@ fun KanbanScreen( items(state.columns) { column -> val colName = column.name val colTasks = - state.tasks.filter { + filteredTasks.filter { it.status.equals(colName, ignoreCase = true) } val columnIndex = state.columns.indexOf(column) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt index b0060401..19c4a2fe 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt @@ -4,7 +4,6 @@ import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -46,6 +45,9 @@ import com.m57.hermescontrol.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState +import com.m57.hermescontrol.ui.common.SearchBar +import com.m57.hermescontrol.ui.common.listContentPadding +import com.m57.hermescontrol.ui.common.listItemSpacing @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -57,6 +59,16 @@ fun KeysScreen( val state by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current + var query by remember { mutableStateOf("") } + + val filteredEnvVars = + remember(query, state.envVars) { + state.envVars.filter { (key, config) -> + key.contains(query, ignoreCase = true) || + config.description?.contains(query, ignoreCase = true) == true + } + } + LaunchedEffect(Unit) { viewModel.loadKeys() } @@ -110,10 +122,17 @@ fun KeysScreen( } else { LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = listContentPadding, + verticalArrangement = listItemSpacing, ) { - items(state.envVars.toList()) { (key, config) -> + item { + SearchBar( + query = query, + onQueryChange = { query = it }, + placeholder = "Search keys...", + ) + } + items(filteredEnvVars.toList()) { (key, config) -> var isEditing by remember { mutableStateOf(false) } val revealedVal = state.revealedValues[key] val isRevealed = revealedVal != null diff --git a/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsScreen.kt index 51ea9a49..9ab64175 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsScreen.kt @@ -16,6 +16,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -30,6 +33,7 @@ import com.m57.hermescontrol.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState +import com.m57.hermescontrol.ui.common.SearchBar @Composable fun LogsScreen( @@ -42,6 +46,15 @@ fun LogsScreen( val spacing = LocalSpacing.current val listState = rememberLazyListState() + var query by remember { mutableStateOf("") } + + val filteredLogs = + remember(query, state.logs) { + state.logs.filter { logLine -> + logLine.contains(query, ignoreCase = true) + } + } + LaunchedEffect(Unit) { viewModel.loadLogs() } @@ -54,9 +67,9 @@ fun LogsScreen( } // Auto scroll to bottom when new logs arrive - LaunchedEffect(state.logs.size) { - if (state.logs.isNotEmpty()) { - listState.animateScrollToItem(state.logs.size - 1) + LaunchedEffect(filteredLogs.size) { + if (filteredLogs.isNotEmpty()) { + listState.animateScrollToItem(filteredLogs.size) } } @@ -101,7 +114,15 @@ fun LogsScreen( vertical = spacing.sm, ), ) { - items(state.logs) { logLine -> + item { + SearchBar( + query = query, + onQueryChange = { query = it }, + placeholder = "Search logs...", + modifier = Modifier.padding(bottom = 8.dp), + ) + } + items(filteredLogs) { logLine -> Text( text = logLine, color = MaterialTheme.colorScheme.onSurface, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt index 39da6410..8577d5de 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt @@ -4,7 +4,6 @@ import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -25,6 +24,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -38,6 +40,9 @@ import com.m57.hermescontrol.R import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState +import com.m57.hermescontrol.ui.common.SearchBar +import com.m57.hermescontrol.ui.common.listContentPadding +import com.m57.hermescontrol.ui.common.listItemSpacing @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -49,6 +54,17 @@ fun McpServersScreen( val state by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current + var query by remember { mutableStateOf("") } + + val filteredServers = + remember(query, state.servers) { + state.servers.filter { server -> + server.name.contains(query, ignoreCase = true) || + server.command?.contains(query, ignoreCase = true) == true || + server.transport?.contains(query, ignoreCase = true) == true + } + } + LaunchedEffect(Unit) { viewModel.loadServers() } @@ -97,10 +113,17 @@ fun McpServersScreen( } else { LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = listContentPadding, + verticalArrangement = listItemSpacing, ) { - items(state.servers) { server -> + item { + SearchBar( + query = query, + onQueryChange = { query = it }, + placeholder = "Search MCP servers...", + ) + } + items(filteredServers) { server -> Card( modifier = Modifier.fillMaxWidth(), colors = diff --git a/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt index 27036d4a..9e8deb79 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt @@ -45,6 +45,7 @@ import com.m57.hermescontrol.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState +import com.m57.hermescontrol.ui.common.SearchBar @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -56,6 +57,18 @@ fun ModelScreen( val state by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current var expandedProviderSlug by remember { mutableStateOf(null) } + var query by remember { mutableStateOf("") } + + val filteredProviders = + remember(query, state.providers) { + state.providers.filter { provider -> + provider.name.contains(query, ignoreCase = true) || + provider.slug.contains(query, ignoreCase = true) || + provider.models.orEmpty().any { model -> + model.contains(query, ignoreCase = true) + } + } + } LaunchedEffect(Unit) { viewModel.loadModelOptions() @@ -121,7 +134,14 @@ fun ModelScreen( contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { - items(state.providers) { provider -> + item { + SearchBar( + query = query, + onQueryChange = { query = it }, + placeholder = "Search models...", + ) + } + items(filteredProviders) { provider -> val isExpanded = expandedProviderSlug == provider.slug val isCurrent = provider.is_current == true diff --git a/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt index 6b1d1db5..bac6281e 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt @@ -4,7 +4,6 @@ import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -25,6 +24,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -37,6 +39,9 @@ import com.m57.hermescontrol.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState +import com.m57.hermescontrol.ui.common.SearchBar +import com.m57.hermescontrol.ui.common.listContentPadding +import com.m57.hermescontrol.ui.common.listItemSpacing @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -48,6 +53,16 @@ fun PluginsScreen( val state by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current + var query by remember { mutableStateOf("") } + + val filteredPlugins = + remember(query, state.plugins) { + state.plugins.filter { plugin -> + plugin.name.contains(query, ignoreCase = true) || + plugin.description?.contains(query, ignoreCase = true) == true + } + } + LaunchedEffect(Unit) { viewModel.loadPlugins() } @@ -101,10 +116,17 @@ fun PluginsScreen( } else { LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = listContentPadding, + verticalArrangement = listItemSpacing, ) { - items(state.plugins) { plugin -> + item { + SearchBar( + query = query, + onQueryChange = { query = it }, + placeholder = "Search plugins...", + ) + } + items(filteredPlugins) { plugin -> Card(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(16.dp)) { Row( diff --git a/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt index 3ed18984..97b3fb34 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt @@ -19,6 +19,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -33,6 +36,7 @@ import com.m57.hermescontrol.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState +import com.m57.hermescontrol.ui.common.SearchBar import com.m57.hermescontrol.ui.common.listContentPadding import com.m57.hermescontrol.ui.common.listItemSpacing @@ -45,6 +49,16 @@ fun SessionsScreen( val state by viewModel.uiState.collectAsStateWithLifecycle() val spacing = LocalSpacing.current + var query by remember { mutableStateOf("") } + + val filteredSessions = + remember(query, state.sessions) { + state.sessions.filter { session -> + session.title?.contains(query, ignoreCase = true) == true || + session.status?.contains(query, ignoreCase = true) == true + } + } + LaunchedEffect(Unit) { viewModel.loadSessions() } @@ -82,7 +96,14 @@ fun SessionsScreen( contentPadding = listContentPadding, verticalArrangement = listItemSpacing, ) { - items(state.sessions) { session -> + item { + SearchBar( + query = query, + onQueryChange = { query = it }, + placeholder = "Search sessions...", + ) + } + items(filteredSessions) { session -> Card( modifier = Modifier diff --git a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsScreen.kt index 1e2b71ea..0843f9b0 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsScreen.kt @@ -63,6 +63,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.m57.hermescontrol.BuildConfig 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.common.HermesScaffold @@ -478,6 +479,51 @@ fun SettingsScreen( // Navigation Bar section SectionCard(title = stringResource(R.string.settings_sec_nav_bar)) { + 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 = state.bottomNavDisplayMode == mode, + onClick = { viewModel.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, state.selectedNavItems.size), style = MaterialTheme.typography.bodyMedium, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt index 17a5a039..bdd8d433 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt @@ -7,6 +7,7 @@ import com.m57.hermescontrol.data.local.AuthManager import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall +import com.m57.hermescontrol.theme.BottomNavDisplayMode import com.m57.hermescontrol.theme.ThemePreference import com.m57.hermescontrol.theme.ThemePreset import kotlinx.coroutines.Dispatchers @@ -34,6 +35,7 @@ data class SettingsUiState( val profiles: List = emptyList(), val selectedProfileId: String? = null, val renameProfileName: String = "", + val bottomNavDisplayMode: BottomNavDisplayMode = BottomNavDisplayMode.ICON_AND_TEXT, ) class SettingsViewModel : ViewModel() { @@ -69,6 +71,7 @@ class SettingsViewModel : ViewModel() { .firstOrNull { p -> p.id == selectedId }?.name ?: "", + bottomNavDisplayMode = AuthManager.getBottomNavDisplayMode(), ) } } @@ -137,6 +140,11 @@ class SettingsViewModel : ViewModel() { AuthManager.setThemePreset(preset) } + 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) } } @@ -205,6 +213,7 @@ class SettingsViewModel : ViewModel() { AuthManager.setThemePreference(state.themePreference) AuthManager.setUseDynamicColors(state.useDynamicColors) AuthManager.setThemePreset(state.themePreset) + AuthManager.setBottomNavDisplayMode(state.bottomNavDisplayMode) AuthManager.setTypingEffectEnabled(state.typingEffectEnabled) AuthManager.setTypingEffectDelayMs(state.typingEffectDelayMs) ApiClient.rebuild() diff --git a/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsScreen.kt index 028fa680..0ff27f55 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -29,6 +28,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -42,6 +44,9 @@ import com.m57.hermescontrol.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState +import com.m57.hermescontrol.ui.common.SearchBar +import com.m57.hermescontrol.ui.common.listContentPadding +import com.m57.hermescontrol.ui.common.listItemSpacing @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -53,6 +58,17 @@ fun ToolsetsScreen( val state by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current + var query by remember { mutableStateOf("") } + + val filteredToolsets = + remember(query, state.toolsets) { + state.toolsets.filter { toolset -> + toolset.name.contains(query, ignoreCase = true) || + toolset.label?.contains(query, ignoreCase = true) == true || + toolset.description?.contains(query, ignoreCase = true) == true + } + } + LaunchedEffect(Unit) { viewModel.loadToolsets() } @@ -114,10 +130,17 @@ fun ToolsetsScreen( } else { LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = listContentPadding, + verticalArrangement = listItemSpacing, ) { - items(state.toolsets) { toolset -> + item { + SearchBar( + query = query, + onQueryChange = { query = it }, + placeholder = "Search toolsets...", + ) + } + items(filteredToolsets) { toolset -> Card( modifier = Modifier.fillMaxWidth(), colors = diff --git a/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt index f08d2071..8c312279 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -26,6 +25,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -39,6 +41,9 @@ import com.m57.hermescontrol.R import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState +import com.m57.hermescontrol.ui.common.SearchBar +import com.m57.hermescontrol.ui.common.listContentPadding +import com.m57.hermescontrol.ui.common.listItemSpacing @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -50,6 +55,16 @@ fun WebhooksScreen( val state by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current + var query by remember { mutableStateOf("") } + + val filteredSubscriptions = + remember(query, state.subscriptions) { + state.subscriptions.filter { sub -> + sub.name.contains(query, ignoreCase = true) || + sub.url.contains(query, ignoreCase = true) + } + } + LaunchedEffect(Unit) { viewModel.loadWebhooks() } @@ -98,8 +113,8 @@ fun WebhooksScreen( } else { LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = listContentPadding, + verticalArrangement = listItemSpacing, ) { // Global Status Switch item { @@ -155,7 +170,15 @@ fun WebhooksScreen( ) } - if (state.subscriptions.isEmpty()) { + item { + SearchBar( + query = query, + onQueryChange = { query = it }, + placeholder = "Search subscriptions...", + ) + } + + if (filteredSubscriptions.isEmpty()) { item { Card( modifier = Modifier.fillMaxWidth(), @@ -176,7 +199,7 @@ fun WebhooksScreen( } } } else { - items(state.subscriptions) { sub -> + items(filteredSubscriptions) { sub -> Card( modifier = Modifier.fillMaxWidth(), ) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2e42adb9..b4d16e12 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -128,6 +128,10 @@ Bottom bar items (%1$d/5) Add Item Maximum of 5 items + Bottom Bar Display Mode + Icon & Text + Icons Only + Text Only ✅ Settings saved Test Connection About