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