feat: UI Changes

* fix(#220): implement edit soul and set model dialogs

* fix(#211): make UI more compact and information dense

* fix(#211): make navigation drawer sidebar more compact

* feat(#252): move settings item to end of navigation drawer

* feat(#250): add bottom navigation bar display mode settings

* feat(#225): add search bar filtering to remaining screens

* fix: make bottom navigation bar display modes more compact

* fix: resolve compilation error in KanbanScreen by referencing assignedTo

* feat: add search to achievements screen and top padding for icon-only navigation bar

* fix: adjust icon-only bottom bar height and remove padding

* fix(#249): remove unused PaddingValues imports in 6 screens

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-06-21 20:42:23 +03:00
1 parent 32def80694
commit 60bf6e8a33
20 files changed
+424 -61

No files matched your search

@@ -44,6 +44,7 @@ import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem 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
@@ -70,6 +71,7 @@ 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 kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.m57.hermescontrol.ui.achievements.AchievementsScreen as AchievementsScreenContent import com.m57.hermescontrol.ui.achievements.AchievementsScreen as AchievementsScreenContent
import com.m57.hermescontrol.ui.channels.ChannelsScreen as ChannelsScreenContent 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), DrawerEntry(GatewayScreen, R.string.screen_gateway, Icons.Filled.Bolt, DrawerSection.AUTOMATE),
// Configure // Configure
DrawerEntry(SkillsScreen, R.string.screen_skills, Icons.Filled.Extension, DrawerSection.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(ToolsetsScreen, R.string.screen_toolsets, Icons.Filled.Build, DrawerSection.CONFIGURE),
DrawerEntry(PluginsScreen, R.string.screen_plugins, Icons.Filled.Memory, DrawerSection.CONFIGURE), DrawerEntry(PluginsScreen, R.string.screen_plugins, Icons.Filled.Memory, DrawerSection.CONFIGURE),
DrawerEntry(ConfigScreen, R.string.screen_config, Icons.Filled.Code, 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(LogsScreen, R.string.screen_logs, Icons.Filled.HistoryEdu, DrawerSection.INSPECT),
DrawerEntry(KanbanScreen, R.string.screen_kanban, Icons.Filled.Dashboard, 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(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<NavKey> = ALL_NAV_ITEMS.mapTo(mutableSetOf()) { it.key } private val DRAWER_GESTURE_SCREENS: Set<NavKey> = 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 // customises items in Settings. Using collectAsState() ensures the NavigationBar
// recomposes and reflects choices instantly. // recomposes and reflects choices instantly.
val bottomNavItemsState by AuthManager.bottomNavItemsFlow.collectAsState() val bottomNavItemsState by AuthManager.bottomNavItemsFlow.collectAsState()
val bottomNavDisplayMode by AuthManager.bottomNavDisplayModeFlow.collectAsState()
val bottomNavItems = resolveBottomNavItems(bottomNavItemsState) val bottomNavItems = resolveBottomNavItems(bottomNavItemsState)
val bottomNavKeys = remember(bottomNavItems) { bottomNavItems.mapTo(mutableSetOf()) { it.key } } val bottomNavKeys = remember(bottomNavItems) { bottomNavItems.mapTo(mutableSetOf()) { it.key } }
@@ -375,7 +378,7 @@ fun MainNavigation(sessionId: String? = null) {
} }
Row( Row(
verticalAlignment = Alignment.CenterVertically, 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(
text = stringResource(R.string.nav_drawer_title), text = stringResource(R.string.nav_drawer_title),
@@ -395,7 +398,7 @@ fun MainNavigation(sessionId: String? = null) {
} }
Text( Text(
text = stringResource(R.string.nav_drawer_subtitle), 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, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
@@ -406,9 +409,9 @@ fun MainNavigation(sessionId: String? = null) {
text = stringResource(section.titleRes).uppercase(), text = stringResource(section.titleRes).uppercase(),
modifier = modifier =
Modifier.padding( Modifier.padding(
start = 20.dp, start = 16.dp,
top = 16.dp, top = 8.dp,
bottom = 6.dp, bottom = 4.dp,
end = 16.dp, end = 16.dp,
), ),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
@@ -434,7 +437,7 @@ fun MainNavigation(sessionId: String? = null) {
), ),
modifier = modifier =
Modifier Modifier
.padding(horizontal = 12.dp, vertical = 2.dp) .padding(horizontal = 8.dp, vertical = 1.dp)
.testTag( .testTag(
"drawer_${entry.key::class.simpleName?.lowercase()?.removeSuffix( "drawer_${entry.key::class.simpleName?.lowercase()?.removeSuffix(
"screen", "screen",
@@ -455,13 +458,53 @@ fun MainNavigation(sessionId: String? = null) {
contentWindowInsets = WindowInsets.navigationBars, contentWindowInsets = WindowInsets.navigationBars,
bottomBar = { bottomBar = {
if (showBottomBar) { 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 -> 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( NavigationBarItem(
selected = currentScreen == item.key, selected = isSelected,
onClick = { NavigationController.navigateTo(item.key) }, onClick = { NavigationController.navigateTo(item.key) },
icon = { Icon(item.icon, contentDescription = stringResource(item.labelRes)) }, colors =
label = { Text(stringResource(item.labelRes)) }, 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 =
Modifier.testTag( Modifier.testTag(
"nav_${item.key::class.simpleName?.lowercase()?.removeSuffix("screen") ?: ""}", "nav_${item.key::class.simpleName?.lowercase()?.removeSuffix("screen") ?: ""}",
@@ -4,6 +4,7 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey import androidx.security.crypto.MasterKey
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.flow.MutableStateFlow 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_SELECTED_PROFILE_ID = "selected_profile_id"
private const val KEY_USE_DYNAMIC_COLORS = "use_dynamic_colors" private const val KEY_USE_DYNAMIC_COLORS = "use_dynamic_colors"
private const val KEY_THEME_PRESET = "theme_preset" 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_HOST = "127.0.0.1"
private const val DEFAULT_PORT = 9119 private const val DEFAULT_PORT = 9119
@@ -52,6 +54,9 @@ 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 gson = com.google.gson.Gson() private val gson = com.google.gson.Gson()
/** /**
@@ -81,6 +86,7 @@ object AuthManager {
_themePreferenceFlow.value = getThemePreference() _themePreferenceFlow.value = getThemePreference()
_useDynamicColorsFlow.value = isUseDynamicColors() _useDynamicColorsFlow.value = isUseDynamicColors()
_themePresetFlow.value = getThemePreset() _themePresetFlow.value = getThemePreset()
_bottomNavDisplayModeFlow.value = getBottomNavDisplayMode()
} }
} }
@@ -282,4 +288,16 @@ object AuthManager {
fun setTypingEffectDelayMs(delayMs: Int) { fun setTypingEffectDelayMs(delayMs: Int) {
requirePrefs().edit().putInt(KEY_TYPING_EFFECT_DELAY_MS, delayMs).apply() 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
}
} }
@@ -18,12 +18,12 @@ import androidx.compose.ui.unit.dp
* Access in composables via `LocalSpacing.current` or the [Spacing] object. * Access in composables via `LocalSpacing.current` or the [Spacing] object.
*/ */
object Spacing { object Spacing {
val xs = 4.dp val xs = 2.dp
val sm = 8.dp val sm = 6.dp
val md = 16.dp val md = 12.dp
val lg = 24.dp val lg = 18.dp
val xl = 32.dp val xl = 24.dp
val xxl = 48.dp val xxl = 36.dp
} }
/** Alias for backwards-compatibility — existing `Spacing.xs` calls still work. */ /** Alias for backwards-compatibility — existing `Spacing.xs` calls still work. */
@@ -17,6 +17,8 @@ enum class ThemePreference { SYSTEM, LIGHT, DARK }
enum class ThemePreset { DEFAULT, MONOCHROME, GRUVBOX, CATPPUCCIN, AMOLED } enum class ThemePreset { DEFAULT, MONOCHROME, GRUVBOX, CATPPUCCIN, AMOLED }
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 }
@@ -242,8 +244,14 @@ fun HermesControlTheme(
val dynamicAvailable = useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S val dynamicAvailable = useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colorScheme = val colorScheme =
when { when {
dynamicAvailable && darkTheme -> dynamicDarkColorScheme(context) dynamicAvailable && darkTheme -> {
dynamicAvailable && !darkTheme -> dynamicLightColorScheme(context) dynamicDarkColorScheme(context)
}
dynamicAvailable && !darkTheme -> {
dynamicLightColorScheme(context)
}
else -> { else -> {
when (themePreset) { when (themePreset) {
ThemePreset.DEFAULT -> if (darkTheme) HermesDarkColorScheme else HermesLightColorScheme ThemePreset.DEFAULT -> if (darkTheme) HermesDarkColorScheme else HermesLightColorScheme
@@ -269,6 +277,7 @@ fun HermesControlTheme(
infoContainer = Color(0xFF3C3836), infoContainer = Color(0xFF3C3836),
) )
} }
themePreset == ThemePreset.GRUVBOX && !darkTheme -> { themePreset == ThemePreset.GRUVBOX && !darkTheme -> {
HermesStatusColors( HermesStatusColors(
success = Color(0xFF98971A), success = Color(0xFF98971A),
@@ -281,6 +290,7 @@ fun HermesControlTheme(
infoContainer = Color(0xFFEBDBB2), infoContainer = Color(0xFFEBDBB2),
) )
} }
themePreset == ThemePreset.CATPPUCCIN && darkTheme -> { themePreset == ThemePreset.CATPPUCCIN && darkTheme -> {
HermesStatusColors( HermesStatusColors(
success = Color(0xFFA6E3A1), success = Color(0xFFA6E3A1),
@@ -293,6 +303,7 @@ fun HermesControlTheme(
infoContainer = Color(0xFF313244), infoContainer = Color(0xFF313244),
) )
} }
themePreset == ThemePreset.CATPPUCCIN && !darkTheme -> { themePreset == ThemePreset.CATPPUCCIN && !darkTheme -> {
HermesStatusColors( HermesStatusColors(
success = Color(0xFF40A02B), success = Color(0xFF40A02B),
@@ -305,6 +316,7 @@ fun HermesControlTheme(
infoContainer = Color(0xFFE6E9EF), infoContainer = Color(0xFFE6E9EF),
) )
} }
darkTheme -> { darkTheme -> {
HermesStatusColors( HermesStatusColors(
success = StatusGreen, success = StatusGreen,
@@ -317,6 +329,7 @@ fun HermesControlTheme(
infoContainer = StatusBlueContainer, infoContainer = StatusBlueContainer,
) )
} }
else -> { else -> {
HermesStatusColors( HermesStatusColors(
success = Color(0xFF1B873A), success = Color(0xFF1B873A),
@@ -21,6 +21,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource 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.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.LoadingState import com.m57.hermescontrol.ui.common.LoadingState
import com.m57.hermescontrol.ui.common.SearchBar
@Composable @Composable
fun AchievementsScreen( fun AchievementsScreen(
@@ -41,6 +45,16 @@ fun AchievementsScreen(
viewModel: AchievementsViewModel = viewModel { AchievementsViewModel() }, viewModel: AchievementsViewModel = viewModel { AchievementsViewModel() },
) { ) {
val state by viewModel.uiState.collectAsStateWithLifecycle() 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) { LaunchedEffect(Unit) {
viewModel.loadAchievements() viewModel.loadAchievements()
@@ -80,7 +94,14 @@ fun AchievementsScreen(
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.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 val pct = achievement.progress_pct?.toFloat()?.div(100f) ?: 0f
Card( Card(
@@ -3,7 +3,6 @@ package com.m57.hermescontrol.ui.channels
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize 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.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.LoadingState 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 @Composable
fun ChannelsScreen( fun ChannelsScreen(
@@ -49,6 +51,15 @@ fun ChannelsScreen(
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current 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) { LaunchedEffect(Unit) {
viewModel.loadPlatforms() viewModel.loadPlatforms()
} }
@@ -90,10 +101,17 @@ fun ChannelsScreen(
else -> { else -> {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize().padding(paddingValues), modifier = Modifier.fillMaxSize().padding(paddingValues),
contentPadding = PaddingValues(16.dp), contentPadding = listContentPadding,
verticalArrangement = Arrangement.spacedBy(16.dp), 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) } var showConfigureForm by remember { mutableStateOf(false) }
Card(modifier = Modifier.fillMaxWidth()) { Card(modifier = Modifier.fillMaxWidth()) {
@@ -157,7 +157,7 @@ private fun UserBubble(
modifier = modifier =
modifier modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 3.dp), .padding(horizontal = 8.dp, vertical = 2.dp),
contentAlignment = Alignment.CenterEnd, contentAlignment = Alignment.CenterEnd,
) { ) {
val gradientBrush = val gradientBrush =
@@ -208,7 +208,7 @@ private fun UserBubble(
color = Color.Transparent, color = Color.Transparent,
tonalElevation = 0.dp, tonalElevation = 0.dp,
) { ) {
Column(modifier = Modifier.padding(12.dp)) { Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)) {
SelectionContainer { SelectionContainer {
Text( Text(
text = highlightedText, text = highlightedText,
@@ -304,7 +304,7 @@ private fun AssistantBubble(
modifier = modifier =
modifier modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 3.dp), .padding(horizontal = 8.dp, vertical = 2.dp),
contentAlignment = Alignment.CenterStart, contentAlignment = Alignment.CenterStart,
) { ) {
Box { Box {
@@ -335,7 +335,7 @@ private fun AssistantBubble(
), ),
tonalElevation = 1.dp, tonalElevation = 1.dp,
) { ) {
Column(modifier = Modifier.padding(12.dp)) { Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)) {
SelectionContainer { SelectionContainer {
RichText( RichText(
text = message.content, text = message.content,
@@ -402,7 +402,7 @@ private fun SystemBubble(
modifier = modifier =
modifier modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 6.dp), .padding(horizontal = 16.dp, vertical = 4.dp),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Text( Text(
@@ -612,7 +612,7 @@ private fun ToolBubble(
modifier = modifier =
modifier modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 2.dp), .padding(horizontal = 8.dp, vertical = 1.dp),
contentAlignment = Alignment.CenterStart, contentAlignment = Alignment.CenterStart,
) { ) {
Card( Card(
@@ -624,7 +624,7 @@ private fun ToolBubble(
modifier = modifier =
Modifier Modifier
.animateContentSize() .animateContentSize()
.padding(horizontal = 12.dp, vertical = 8.dp), .padding(horizontal = 10.dp, vertical = 6.dp),
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -157,6 +157,6 @@ fun EmptyState(
// ── Padding conventions ────────────────────────────────────────────── // ── 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)
@@ -49,6 +49,7 @@ import com.m57.hermescontrol.data.model.KanbanTask
import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.LoadingState import com.m57.hermescontrol.ui.common.LoadingState
import com.m57.hermescontrol.ui.common.SearchBar
private const val DEFAULT_COLUMN = "todo" private const val DEFAULT_COLUMN = "todo"
@@ -61,6 +62,18 @@ fun KanbanScreen(
) { ) {
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current 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) } var showAddTaskDialog by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -107,6 +120,12 @@ fun KanbanScreen(
Column( Column(
modifier = Modifier.fillMaxSize().padding(paddingValues), 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 // Board selector tab row
if (state.boards.isNotEmpty()) { if (state.boards.isNotEmpty()) {
PrimaryScrollableTabRow( PrimaryScrollableTabRow(
@@ -140,7 +159,7 @@ fun KanbanScreen(
items(state.columns) { column -> items(state.columns) { column ->
val colName = column.name val colName = column.name
val colTasks = val colTasks =
state.tasks.filter { filteredTasks.filter {
it.status.equals(colName, ignoreCase = true) it.status.equals(colName, ignoreCase = true)
} }
val columnIndex = state.columns.indexOf(column) val columnIndex = state.columns.indexOf(column)
@@ -4,7 +4,6 @@ import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize 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.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.LoadingState 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -57,6 +59,16 @@ fun KeysScreen(
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current 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) { LaunchedEffect(Unit) {
viewModel.loadKeys() viewModel.loadKeys()
} }
@@ -110,10 +122,17 @@ fun KeysScreen(
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp), contentPadding = listContentPadding,
verticalArrangement = Arrangement.spacedBy(8.dp), 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) } var isEditing by remember { mutableStateOf(false) }
val revealedVal = state.revealedValues[key] val revealedVal = state.revealedValues[key]
val isRevealed = revealedVal != null val isRevealed = revealedVal != null
@@ -16,6 +16,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue 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.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource 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.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.LoadingState import com.m57.hermescontrol.ui.common.LoadingState
import com.m57.hermescontrol.ui.common.SearchBar
@Composable @Composable
fun LogsScreen( fun LogsScreen(
@@ -42,6 +46,15 @@ fun LogsScreen(
val spacing = LocalSpacing.current val spacing = LocalSpacing.current
val listState = rememberLazyListState() 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) { LaunchedEffect(Unit) {
viewModel.loadLogs() viewModel.loadLogs()
} }
@@ -54,9 +67,9 @@ fun LogsScreen(
} }
// Auto scroll to bottom when new logs arrive // Auto scroll to bottom when new logs arrive
LaunchedEffect(state.logs.size) { LaunchedEffect(filteredLogs.size) {
if (state.logs.isNotEmpty()) { if (filteredLogs.isNotEmpty()) {
listState.animateScrollToItem(state.logs.size - 1) listState.animateScrollToItem(filteredLogs.size)
} }
} }
@@ -101,7 +114,15 @@ fun LogsScreen(
vertical = spacing.sm, 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(
text = logLine, text = logLine,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
@@ -4,7 +4,6 @@ import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -25,6 +24,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext 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.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.LoadingState 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -49,6 +54,17 @@ fun McpServersScreen(
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current 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) { LaunchedEffect(Unit) {
viewModel.loadServers() viewModel.loadServers()
} }
@@ -97,10 +113,17 @@ fun McpServersScreen(
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp), contentPadding = listContentPadding,
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = listItemSpacing,
) { ) {
items(state.servers) { server -> item {
SearchBar(
query = query,
onQueryChange = { query = it },
placeholder = "Search MCP servers...",
)
}
items(filteredServers) { server ->
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = colors =
@@ -45,6 +45,7 @@ import com.m57.hermescontrol.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.LoadingState import com.m57.hermescontrol.ui.common.LoadingState
import com.m57.hermescontrol.ui.common.SearchBar
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -56,6 +57,18 @@ fun ModelScreen(
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current val context = LocalContext.current
var expandedProviderSlug by remember { mutableStateOf<String?>(null) } var expandedProviderSlug by remember { mutableStateOf<String?>(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) { LaunchedEffect(Unit) {
viewModel.loadModelOptions() viewModel.loadModelOptions()
@@ -121,7 +134,14 @@ fun ModelScreen(
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.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 isExpanded = expandedProviderSlug == provider.slug
val isCurrent = provider.is_current == true val isCurrent = provider.is_current == true
@@ -4,7 +4,6 @@ import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -25,6 +24,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext 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.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.LoadingState 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -48,6 +53,16 @@ fun PluginsScreen(
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current 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) { LaunchedEffect(Unit) {
viewModel.loadPlugins() viewModel.loadPlugins()
} }
@@ -101,10 +116,17 @@ fun PluginsScreen(
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp), contentPadding = listContentPadding,
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = listItemSpacing,
) { ) {
items(state.plugins) { plugin -> item {
SearchBar(
query = query,
onQueryChange = { query = it },
placeholder = "Search plugins...",
)
}
items(filteredPlugins) { plugin ->
Card(modifier = Modifier.fillMaxWidth()) { Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
Row( Row(
@@ -19,6 +19,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource 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.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.LoadingState 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.listContentPadding
import com.m57.hermescontrol.ui.common.listItemSpacing import com.m57.hermescontrol.ui.common.listItemSpacing
@@ -45,6 +49,16 @@ fun SessionsScreen(
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
val spacing = LocalSpacing.current 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) { LaunchedEffect(Unit) {
viewModel.loadSessions() viewModel.loadSessions()
} }
@@ -82,7 +96,14 @@ fun SessionsScreen(
contentPadding = listContentPadding, contentPadding = listContentPadding,
verticalArrangement = listItemSpacing, verticalArrangement = listItemSpacing,
) { ) {
items(state.sessions) { session -> item {
SearchBar(
query = query,
onQueryChange = { query = it },
placeholder = "Search sessions...",
)
}
items(filteredSessions) { session ->
Card( Card(
modifier = modifier =
Modifier Modifier
@@ -63,6 +63,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.BuildConfig import com.m57.hermescontrol.BuildConfig
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.common.HermesScaffold import com.m57.hermescontrol.ui.common.HermesScaffold
@@ -478,6 +479,51 @@ fun SettingsScreen(
// Navigation Bar section // Navigation Bar section
SectionCard(title = stringResource(R.string.settings_sec_nav_bar)) { 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(
text = stringResource(R.string.settings_item_nav_items, state.selectedNavItems.size), text = stringResource(R.string.settings_item_nav_items, state.selectedNavItems.size),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
@@ -7,6 +7,7 @@ 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.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.NetworkResult
import com.m57.hermescontrol.data.remote.safeApiCall import com.m57.hermescontrol.data.remote.safeApiCall
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.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -34,6 +35,7 @@ data class SettingsUiState(
val profiles: List<com.m57.hermescontrol.data.model.ConnectionProfile> = emptyList(), val profiles: List<com.m57.hermescontrol.data.model.ConnectionProfile> = emptyList(),
val selectedProfileId: String? = null, val selectedProfileId: String? = null,
val renameProfileName: String = "", val renameProfileName: String = "",
val bottomNavDisplayMode: BottomNavDisplayMode = BottomNavDisplayMode.ICON_AND_TEXT,
) )
class SettingsViewModel : ViewModel() { class SettingsViewModel : ViewModel() {
@@ -69,6 +71,7 @@ class SettingsViewModel : ViewModel() {
.firstOrNull { p -> .firstOrNull { p ->
p.id == selectedId p.id == selectedId
}?.name ?: "", }?.name ?: "",
bottomNavDisplayMode = AuthManager.getBottomNavDisplayMode(),
) )
} }
} }
@@ -137,6 +140,11 @@ class SettingsViewModel : ViewModel() {
AuthManager.setThemePreset(preset) AuthManager.setThemePreset(preset)
} }
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) }
} }
@@ -205,6 +213,7 @@ class SettingsViewModel : ViewModel() {
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()
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -29,6 +28,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext 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.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.LoadingState 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -53,6 +58,17 @@ fun ToolsetsScreen(
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current 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) { LaunchedEffect(Unit) {
viewModel.loadToolsets() viewModel.loadToolsets()
} }
@@ -114,10 +130,17 @@ fun ToolsetsScreen(
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp), contentPadding = listContentPadding,
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = listItemSpacing,
) { ) {
items(state.toolsets) { toolset -> item {
SearchBar(
query = query,
onQueryChange = { query = it },
placeholder = "Search toolsets...",
)
}
items(filteredToolsets) { toolset ->
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = colors =
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -26,6 +25,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext 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.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.LoadingState 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -50,6 +55,16 @@ fun WebhooksScreen(
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current 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) { LaunchedEffect(Unit) {
viewModel.loadWebhooks() viewModel.loadWebhooks()
} }
@@ -98,8 +113,8 @@ fun WebhooksScreen(
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp), contentPadding = listContentPadding,
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = listItemSpacing,
) { ) {
// Global Status Switch // Global Status Switch
item { 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 { item {
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -176,7 +199,7 @@ fun WebhooksScreen(
} }
} }
} else { } else {
items(state.subscriptions) { sub -> items(filteredSubscriptions) { sub ->
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
+4
View File
@@ -128,6 +128,10 @@
<string name="settings_item_nav_items">Bottom bar items (%1$d/5)</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_action_add_item">Add Item</string>
<string name="settings_nav_max_reached">Maximum of 5 items</string> <string name="settings_nav_max_reached">Maximum of 5 items</string>
<string name="settings_item_bottom_nav_display_mode">Bottom Bar Display Mode</string>
<string name="settings_display_mode_icon_and_text">Icon &amp; Text</string>
<string name="settings_display_mode_icon_only">Icons Only</string>
<string name="settings_display_mode_text_only">Text Only</string>
<string name="settings_save_success">✅ Settings saved</string> <string name="settings_save_success">✅ Settings saved</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>