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

@@ -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(
@@ -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()) {
@@ -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,
@@ -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)
@@ -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)
@@ -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
@@ -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,
@@ -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 =
@@ -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<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) {
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
@@ -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(
@@ -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
@@ -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,
@@ -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<com.m57.hermescontrol.data.model.ConnectionProfile> = 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()
@@ -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 =
@@ -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(),
) {