From 4892572bccc0839b6b1e748eee7a3bf9caf64ccf Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Thu, 18 Jun 2026 18:58:08 +0300 Subject: [PATCH] feat(#13): add pull-to-refresh to HermesScaffold (#14) - HermesScaffold: added isRefreshing param, wraps content in PullToRefreshBox when onRefresh is set - All 16 data screens: pass isRefreshing = state.isLoading - Initial loading only shows full-screen LoadingState when data is empty (no spinner over existing content) - AchievementsScreen already had the correct pattern, just needed isRefreshing param Co-authored-by: M57 --- .../ui/achievements/AchievementsScreen.kt | 1 + .../ui/channels/ChannelsScreen.kt | 3 +- .../hermescontrol/ui/common/HermesScaffold.kt | 30 ++++++++++++++----- .../hermescontrol/ui/config/ConfigScreen.kt | 3 +- .../hermescontrol/ui/cron/CronJobsScreen.kt | 3 +- .../hermescontrol/ui/gateway/GatewayScreen.kt | 3 +- .../hermescontrol/ui/kanban/KanbanScreen.kt | 3 +- .../m57/hermescontrol/ui/keys/KeysScreen.kt | 3 +- .../m57/hermescontrol/ui/logs/LogsScreen.kt | 3 +- .../hermescontrol/ui/mcp/McpServersScreen.kt | 3 +- .../m57/hermescontrol/ui/model/ModelScreen.kt | 3 +- .../hermescontrol/ui/pairing/PairingScreen.kt | 3 +- .../hermescontrol/ui/plugins/PluginsScreen.kt | 3 +- .../ui/profiles/ProfilesScreen.kt | 3 +- .../hermescontrol/ui/skills/SkillsScreen.kt | 3 +- .../hermescontrol/ui/system/SystemScreen.kt | 3 +- .../ui/toolsets/ToolsetsScreen.kt | 3 +- .../ui/webhooks/WebhooksScreen.kt | 3 +- 18 files changed, 56 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsScreen.kt index b1f5ac1a..c0b4b8f1 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsScreen.kt @@ -47,6 +47,7 @@ fun AchievementsScreen( HermesScaffold( title = { Text("Agent Achievements") }, onOpenDrawer = onOpenDrawer, + isRefreshing = state.isLoading, onRefresh = { viewModel.loadAchievements() }, ) { paddingValues -> when { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt index 18d28b46..f671979f 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt @@ -61,10 +61,11 @@ fun ChannelsScreen( HermesScaffold( title = { Text("Messaging Channels") }, onOpenDrawer = onOpenDrawer, + isRefreshing = state.isLoading, onRefresh = { viewModel.loadPlatforms() }, ) { paddingValues -> when { - state.isLoading -> { + state.isLoading && state.platforms.isEmpty() -> { LoadingState(modifier = Modifier.padding(paddingValues)) } state.errorMessage != null -> { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/common/HermesScaffold.kt b/app/src/main/java/com/m57/hermescontrol/ui/common/HermesScaffold.kt index bebc5850..47d32fad 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/common/HermesScaffold.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/common/HermesScaffold.kt @@ -15,6 +15,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -38,6 +39,7 @@ fun HermesScaffold( title: @Composable () -> Unit, onOpenDrawer: (() -> Unit)? = null, onRefresh: (() -> Unit)? = null, + isRefreshing: Boolean = false, onBack: (() -> Unit)? = null, showBack: Boolean = false, snackbarHost: @Composable () -> Unit = {}, @@ -95,13 +97,27 @@ fun HermesScaffold( }, containerColor = MaterialTheme.colorScheme.background, ) { paddingValues -> - Box( - modifier = - Modifier - .fillMaxSize() - .padding(paddingValues), - ) { - content(paddingValues) + val refreshContent: @Composable () -> Unit = { + Box( + modifier = + Modifier + .fillMaxSize() + .padding(paddingValues), + ) { + content(paddingValues) + } + } + + if (onRefresh != null) { + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = onRefresh, + modifier = Modifier.fillMaxSize(), + ) { + refreshContent() + } + } else { + refreshContent() } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigScreen.kt index 9b5e6baa..22a3542c 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigScreen.kt @@ -59,10 +59,11 @@ fun ConfigScreen( HermesScaffold( title = { Text("Configuration") }, onOpenDrawer = onOpenDrawer, + isRefreshing = state.isLoading, onRefresh = { viewModel.loadRawConfig() }, ) { paddingValues -> when { - state.isLoading -> { + state.isLoading && state.yamlText == null -> { LoadingState(modifier = Modifier.padding(paddingValues)) } state.errorMessage != null -> { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsScreen.kt index cefa7164..5f9b4a46 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsScreen.kt @@ -61,10 +61,11 @@ fun CronJobsScreen( HermesScaffold( title = { Text("Cron Jobs") }, onOpenDrawer = onOpenDrawer, + isRefreshing = state.isLoading, onRefresh = { viewModel.loadCronJobs() }, ) { when { - state.isLoading -> LoadingState() + state.isLoading && state.jobs.isEmpty() -> LoadingState() state.errorMessage != null -> ErrorState( message = state.errorMessage ?: "Unknown error", diff --git a/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayScreen.kt index 0b1d528c..eb589ea0 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayScreen.kt @@ -64,10 +64,11 @@ fun GatewayScreen( HermesScaffold( title = { Text("Gateway Control") }, onOpenDrawer = onOpenDrawer, + isRefreshing = state.isLoading, onRefresh = { viewModel.loadStatus() }, ) { paddingValues -> when { - state.isLoading -> { + state.isLoading && state.status == null -> { LoadingState(modifier = Modifier.padding(paddingValues)) } state.errorMessage != null -> { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt index 640965aa..a9edac36 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt @@ -73,10 +73,11 @@ fun KanbanScreen( HermesScaffold( title = { Text("Kanban Board") }, onOpenDrawer = onOpenDrawer, + isRefreshing = state.isLoading, onRefresh = { viewModel.loadBoards() }, ) { paddingValues -> when { - state.isLoading -> { + state.isLoading && state.boards.isEmpty() -> { LoadingState(modifier = Modifier.padding(paddingValues)) } state.errorMessage != null -> { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt index 1f68167a..3436811f 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt @@ -68,10 +68,11 @@ fun KeysScreen( HermesScaffold( title = { Text("Keys & Credentials") }, onOpenDrawer = onOpenDrawer, + isRefreshing = state.isLoading, onRefresh = { viewModel.loadKeys() }, ) { paddingValues -> when { - state.isLoading -> { + state.isLoading && state.envVars.isEmpty() -> { LoadingState(modifier = Modifier.padding(paddingValues)) } state.errorMessage != null -> { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsScreen.kt index f26401c6..879e5ecd 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsScreen.kt @@ -61,10 +61,11 @@ fun LogsScreen( HermesScaffold( title = { Text("Logs") }, onOpenDrawer = onOpenDrawer, + isRefreshing = state.isLoading, onRefresh = { viewModel.loadLogs() }, ) { when { - state.isLoading -> LoadingState() + state.isLoading && state.logs.isEmpty() -> LoadingState() state.errorMessage != null -> ErrorState( message = state.errorMessage ?: "Unknown error", diff --git a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt index acbb3d1f..98257b14 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt @@ -61,10 +61,11 @@ fun McpServersScreen( HermesScaffold( title = { Text("MCP Servers") }, onOpenDrawer = onOpenDrawer, + isRefreshing = state.isLoading, onRefresh = { viewModel.loadServers() }, ) { paddingValues -> when { - state.isLoading -> { + state.isLoading && state.servers.isEmpty() -> { LoadingState(modifier = Modifier.padding(paddingValues)) } state.errorMessage != null -> { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt index 9a6f7742..0ac80e88 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt @@ -68,10 +68,11 @@ fun ModelScreen( HermesScaffold( title = { Text("Models") }, onOpenDrawer = onOpenDrawer, + isRefreshing = state.isLoading, onRefresh = { viewModel.loadModelOptions() }, ) { paddingValues -> when { - state.isLoading -> { + state.isLoading && state.providers.isEmpty() -> { LoadingState(modifier = Modifier.padding(paddingValues)) } state.errorMessage != null -> { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingScreen.kt index 2eb36e88..4bc1f4ef 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingScreen.kt @@ -60,10 +60,11 @@ fun PairingScreen( HermesScaffold( title = { Text("Device Pairing") }, onOpenDrawer = onOpenDrawer, + isRefreshing = state.isLoading, onRefresh = { viewModel.loadPairing() }, ) { paddingValues -> when { - state.isLoading -> { + state.isLoading && state.pairing == null -> { LoadingState(modifier = Modifier.padding(paddingValues)) } state.errorMessage != null -> { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt index 51a2aaf4..f8b2f727 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt @@ -59,10 +59,11 @@ fun PluginsScreen( HermesScaffold( title = { Text("Plugins") }, onOpenDrawer = onOpenDrawer, + isRefreshing = state.isLoading, onRefresh = { viewModel.loadPlugins() }, ) { paddingValues -> when { - state.isLoading -> { + state.isLoading && state.plugins.isEmpty() -> { LoadingState(modifier = Modifier.padding(paddingValues)) } state.errorMessage != null -> { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesScreen.kt index f830bd76..7b794b8f 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesScreen.kt @@ -72,10 +72,11 @@ fun ProfilesScreen( HermesScaffold( title = { Text("Profiles") }, onOpenDrawer = onOpenDrawer, + isRefreshing = state.isLoading, onRefresh = { viewModel.loadProfiles() }, ) { paddingValues -> when { - state.isLoading -> { + state.isLoading && state.profiles.isEmpty() -> { LoadingState(modifier = Modifier.padding(paddingValues)) } state.errorMessage != null -> { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt index 9cc9f98c..511bcdf4 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt @@ -74,10 +74,11 @@ fun SkillsScreen( HermesScaffold( title = { Text("Skills") }, onOpenDrawer = onOpenDrawer, + isRefreshing = state.isLoading, onRefresh = { viewModel.loadSkills() }, ) { when { - state.isLoading -> LoadingState() + state.isLoading && state.skills.isEmpty() -> LoadingState() state.errorMessage != null -> ErrorState( message = state.errorMessage ?: "Unknown error", diff --git a/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt index 96f8bfe9..a5333783 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt @@ -67,10 +67,11 @@ fun SystemScreen( HermesScaffold( title = { Text("System") }, onOpenDrawer = onOpenDrawer, + isRefreshing = state.isLoading, onRefresh = { viewModel.loadSystemData() }, ) { when { - state.isLoading -> LoadingState() + state.isLoading && state.stats == null && state.doctorReport == null -> LoadingState() state.errorMessage != null -> ErrorState( message = state.errorMessage ?: "Unknown error", diff --git a/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsScreen.kt index 2288d150..d81e305c 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsScreen.kt @@ -64,10 +64,11 @@ fun ToolsetsScreen( HermesScaffold( title = { Text("Toolsets") }, onOpenDrawer = onOpenDrawer, + isRefreshing = state.isLoading, onRefresh = { viewModel.loadToolsets() }, ) { paddingValues -> when { - state.isLoading -> { + state.isLoading && state.toolsets.isEmpty() -> { LoadingState(modifier = Modifier.padding(paddingValues)) } state.errorMessage != null -> { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt index c9124cad..543ed871 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt @@ -62,10 +62,11 @@ fun WebhooksScreen( HermesScaffold( title = { Text("Webhooks") }, onOpenDrawer = onOpenDrawer, + isRefreshing = state.isLoading, onRefresh = { viewModel.loadWebhooks() }, ) { paddingValues -> when { - state.isLoading -> { + state.isLoading && state.baseUrl == null -> { LoadingState(modifier = Modifier.padding(paddingValues)) } state.errorMessage != null -> {