feat(#617): skeleton loaders instead of bare spinner on list screens (#625)

Adds SkeletonListState to StateViews.kt — an animated shimmer
placeholder list whose geometry matches a typical list row (avatar circle
+ title line + subtitle line). The shimmer colors derive from
MaterialTheme.colorScheme.surfaceVariant (theme-aware, works on
light/dark/AMOLED/Material You). Sweeps horizontally via an
infiniteTransition tween — no new dependencies, pure Compose foundation
animations.

Wired SkeletonListState into 22 list-loading call sites across 22 screens:
skills, cron, plugins, mcp, toolsets, webhooks, system, sessions (x2),
config (x2 — list + YAMLEditor), model, profiles, keys, channels,
achievements, gateways, kanban, logs, analytics, providers,
processes, hub skills tab, installed skills tab (list branch).

Preserved LoadingState() (centered spinner) where the issue explicitly
calls for it:
- PairingScreen (one-shot auth flow)
- SkillEditorDialog / ModelPickerDialog / CronJobEditorDialog interiors
- SkillPreviewDialog and SkillEditorDialog inline-loading branches in
  SkillsScreen.kt (lines 790, 914)
- InstalledSkillsTab dialog-interior branch (line 393)

SkeletonListState testTag: "skeleton_list_state".

Verified: ktlintCheck, assembleDebug, testDebugUnitTest all green locally.

Co-authored-by: Hy4ri <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andHy4ri authored and GitHub committed 2026-07-18 15:12:21 +03:00
1 parent 0a0b612d3a
commit 0018e5d172
23 files changed
+186 -53

No files matched your search

@@ -57,10 +57,10 @@ import com.m57.hermescontrol.data.model.RecentUnlock
import com.m57.hermescontrol.ui.common.EmptyState 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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SearchBar import com.m57.hermescontrol.ui.common.SearchBar
import com.m57.hermescontrol.ui.common.SectionHeader import com.m57.hermescontrol.ui.common.SectionHeader
import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.StatusBadge import com.m57.hermescontrol.ui.common.StatusBadge
import com.m57.hermescontrol.ui.common.StatusBadgeType import com.m57.hermescontrol.ui.common.StatusBadgeType
import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.ToastEffect
@@ -169,7 +169,7 @@ fun AchievementsScreen(
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading && state.achievements.isEmpty() -> { state.isLoading && state.achievements.isEmpty() -> {
LoadingState(modifier = Modifier.padding(paddingValues)) SkeletonListState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null && state.achievements.isEmpty() -> { state.errorMessage != null && state.achievements.isEmpty() -> {
@@ -46,8 +46,8 @@ import com.m57.hermescontrol.theme.LocalHermesStatusColors
import com.m57.hermescontrol.ui.common.EmptyState 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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SkeletonListState
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
@@ -74,7 +74,7 @@ fun AnalyticsScreen(
) { ) {
when { when {
state.isLoading && state.usage == null -> { state.isLoading && state.usage == null -> {
LoadingState() SkeletonListState()
} }
state.errorMessage != null && state.usage == null -> { state.errorMessage != null && state.usage == null -> {
@@ -50,9 +50,9 @@ import com.m57.hermescontrol.ui.channels.components.TelegramOnboardingDialog
import com.m57.hermescontrol.ui.common.EmptyState 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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SearchBar import com.m57.hermescontrol.ui.common.SearchBar
import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.ToastEffect
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
@@ -132,10 +132,7 @@ fun ChannelsScreen(
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading && state.platforms.isEmpty() -> { state.isLoading && state.platforms.isEmpty() -> {
LoadingState( SkeletonListState(modifier = Modifier.padding(paddingValues))
subtitle = stringResource(R.string.loading_state_subtitle_channels),
modifier = Modifier.padding(paddingValues),
)
} }
state.errorMessage != null && state.platforms.isEmpty() -> { state.errorMessage != null && state.platforms.isEmpty() -> {
@@ -1,14 +1,26 @@
package com.m57.hermescontrol.ui.common package com.m57.hermescontrol.ui.common
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
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.PaddingValues
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
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Inbox import androidx.compose.material.icons.filled.Inbox
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
@@ -19,12 +31,20 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.m57.hermescontrol.R import com.m57.hermescontrol.R
import com.m57.hermescontrol.theme.LocalSpacing import com.m57.hermescontrol.theme.LocalSpacing
@@ -168,6 +188,125 @@ fun EmptyState(
} }
} }
// ── Skeleton (shimmer) — see issue #617 ────────────────────────────────
//
// Skeleton loaders for list screens. Replaces bare `CircularProgressIndicator`
// in list screens to (1) reduce perceived latency (~2× per NN/g research)
// and (2) eliminate layout shift when real content lands. Pair with
// `LoadingState()` for centered/one-shot contexts (Pairing flow, dialog
// interiors) where a spinner is still appropriate.
/**
* An animated shimmer [Brush] derived from the current theme's
* `MaterialTheme.colorScheme.surfaceVariant`. Sweeps horizontally across the
* canvas; offset driven by an infinite `1.2s` tween so the shimmer never
* freezes. Theme-derived — works on light, dark, AMOLED, Material You.
*/
@Composable
private fun shimmerBrush(): Brush {
val base = MaterialTheme.colorScheme.surfaceVariant
val highlight = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)
val transition = rememberInfiniteTransition(label = "shimmer")
val widthPx = with(LocalDensity.current) { LocalConfiguration.current.screenWidthDp.dp.toPx() }
val offset by transition.animateFloat(
initialValue = -widthPx,
targetValue = widthPx,
animationSpec =
infiniteRepeatable(
animation = tween(durationMillis = 1200, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Restart,
),
label = "shimmer_offset",
)
return Brush.linearGradient(
colors = listOf(base, highlight, base),
start = Offset(offset, 0f),
end = Offset(offset + widthPx, 0f),
)
}
@Composable
private fun ShimmerBox(
widthFraction: Float,
height: Dp,
shape: Shape,
modifier: Modifier = Modifier,
) {
Box(
modifier =
modifier
.height(height)
.fillMaxWidth(widthFraction)
.clip(shape)
.background(shimmerBrush()),
)
}
@Composable
private fun SkeletonRow(height: Dp) {
val spacing = LocalSpacing.current
val shape = RoundedCornerShape(12.dp)
Row(
modifier =
Modifier
.fillMaxWidth()
.height(height)
.padding(vertical = spacing.sm),
verticalAlignment = Alignment.CenterVertically,
) {
// Avatar circle — 40dp to match default LoadingState spinner size.
ShimmerBox(
widthFraction = 0f,
height = 40.dp,
shape = CircleShape,
modifier = Modifier.size(40.dp),
)
Spacer(modifier = Modifier.width(spacing.md))
// Title + subtitle lines
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(spacing.sm),
) {
ShimmerBox(widthFraction = 0.6f, height = 14.dp, shape = shape)
ShimmerBox(widthFraction = 0.4f, height = 12.dp, shape = shape)
}
}
}
/**
* Skeleton placeholder list for loading states on list/management screens.
*
* Renders [itemCount] shimmering rows whose geometry matches the typical list
* row (avatar circle + title line + subtitle line). The shimmer colors derive
* from theme tokens — works on light, dark, AMOLED, and Material You. No
* layout shift on transition to real content (acceptance criterion, issue #617).
*
* @param itemCount number of placeholder rows to render (default 6 — long
* enough to fill a phone viewport under typical content density).
* @param rowHeight height of each placeholder row (default 72dp — same as a
* one-line `NavigationDrawerItem`). Override per-screen to match real row
* geometry when known.
* @param modifier outer modifier; pass `Modifier.padding(paddingValues)` from
* the scaffold so the skeleton offsets for the top bar like real content does.
*/
@Composable
fun SkeletonListState(
itemCount: Int = 6,
rowHeight: Dp = 72.dp,
modifier: Modifier = Modifier,
) {
Column(
modifier =
modifier
.fillMaxSize()
.padding(horizontal = 12.dp)
.testTag("skeleton_list_state"),
verticalArrangement = listItemSpacing,
) {
repeat(itemCount) { SkeletonRow(height = rowHeight) }
}
}
// ── Padding conventions ────────────────────────────────────────────── // ── Padding conventions ──────────────────────────────────────────────
val listContentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp) val listContentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp)
@@ -58,9 +58,9 @@ import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.model.SchemaField import com.m57.hermescontrol.data.model.SchemaField
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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SearchBar import com.m57.hermescontrol.ui.common.SearchBar
import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.ToastEffect
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
@@ -101,7 +101,7 @@ fun ConfigScreen(
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading && state.config == null -> { state.isLoading && state.config == null -> {
LoadingState(modifier = Modifier.padding(paddingValues)) SkeletonListState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null && state.config == null -> { state.errorMessage != null && state.config == null -> {
@@ -281,7 +281,7 @@ private fun YAMLEditor(
onSave: () -> Unit, onSave: () -> Unit,
) { ) {
if (isLoading) { if (isLoading) {
LoadingState() SkeletonListState()
return return
} }
@@ -57,6 +57,7 @@ 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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.StatusBadge import com.m57.hermescontrol.ui.common.StatusBadge
import com.m57.hermescontrol.ui.common.StatusBadgeType import com.m57.hermescontrol.ui.common.StatusBadgeType
import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.ToastEffect
@@ -96,7 +97,7 @@ fun CronJobsScreen(
) { ) {
when { when {
state.isLoading && state.jobs.isEmpty() -> { state.isLoading && state.jobs.isEmpty() -> {
LoadingState() SkeletonListState()
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -38,8 +38,8 @@ import com.m57.hermescontrol.R
import com.m57.hermescontrol.theme.LocalHermesStatusColors import com.m57.hermescontrol.theme.LocalHermesStatusColors
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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.ToastEffect
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -66,7 +66,7 @@ fun GatewayScreen(
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading && state.status == null -> { state.isLoading && state.status == null -> {
LoadingState(modifier = Modifier.padding(paddingValues)) SkeletonListState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -46,9 +46,9 @@ import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.model.KanbanTask 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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SearchBar import com.m57.hermescontrol.ui.common.SearchBar
import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.ToastEffect
private const val DEFAULT_COLUMN = "todo" private const val DEFAULT_COLUMN = "todo"
@@ -94,7 +94,7 @@ fun KanbanScreen(
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading && state.boards.isEmpty() -> { state.isLoading && state.boards.isEmpty() -> {
LoadingState(modifier = Modifier.padding(paddingValues)) SkeletonListState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -51,10 +51,10 @@ import com.m57.hermescontrol.R
import com.m57.hermescontrol.ui.common.EmptyState 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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SearchBar import com.m57.hermescontrol.ui.common.SearchBar
import com.m57.hermescontrol.ui.common.SectionHeader import com.m57.hermescontrol.ui.common.SectionHeader
import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.ToastEffect
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
@@ -106,7 +106,7 @@ fun KeysScreen(
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading && !hasAnyVars -> { state.isLoading && !hasAnyVars -> {
LoadingState(modifier = Modifier.padding(paddingValues)) SkeletonListState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null && !hasAnyVars -> { state.errorMessage != null && !hasAnyVars -> {
@@ -54,9 +54,9 @@ import com.m57.hermescontrol.theme.LocalSpacing
import com.m57.hermescontrol.ui.common.EmptyState 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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SearchBar import com.m57.hermescontrol.ui.common.SearchBar
import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.ToastEffect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -191,7 +191,7 @@ fun LogsScreen(
) { ) {
when { when {
state.isLoading && state.logs.isEmpty() -> { state.isLoading && state.logs.isEmpty() -> {
LoadingState() SkeletonListState()
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -65,10 +65,10 @@ import com.m57.hermescontrol.ui.common.DetailDialog
import com.m57.hermescontrol.ui.common.EmptyState 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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SearchBar import com.m57.hermescontrol.ui.common.SearchBar
import com.m57.hermescontrol.ui.common.SectionHeader import com.m57.hermescontrol.ui.common.SectionHeader
import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.StatusBadge import com.m57.hermescontrol.ui.common.StatusBadge
import com.m57.hermescontrol.ui.common.StatusBadgeType import com.m57.hermescontrol.ui.common.StatusBadgeType
import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.ToastEffect
@@ -120,7 +120,7 @@ fun McpServersScreen(
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading && state.servers.isEmpty() -> { state.isLoading && state.servers.isEmpty() -> {
LoadingState(modifier = Modifier.padding(paddingValues)) SkeletonListState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -58,9 +58,9 @@ import com.m57.hermescontrol.R
import com.m57.hermescontrol.ui.common.EmptyState 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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SearchBar import com.m57.hermescontrol.ui.common.SearchBar
import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.ToastEffect
import com.m57.hermescontrol.ui.model.components.MoaConfigDialog import com.m57.hermescontrol.ui.model.components.MoaConfigDialog
import com.m57.hermescontrol.ui.model.components.ModelPickerDialog import com.m57.hermescontrol.ui.model.components.ModelPickerDialog
@@ -101,10 +101,7 @@ fun ModelScreen(
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading && state.providers.isEmpty() -> { state.isLoading && state.providers.isEmpty() -> {
LoadingState( SkeletonListState(modifier = Modifier.padding(paddingValues))
subtitle = stringResource(R.string.loading_state_subtitle_models),
modifier = Modifier.padding(paddingValues),
)
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -57,9 +57,9 @@ import com.m57.hermescontrol.ui.common.DetailDialog
import com.m57.hermescontrol.ui.common.EmptyState 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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SearchBar import com.m57.hermescontrol.ui.common.SearchBar
import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.ToastEffect
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
@@ -131,7 +131,7 @@ fun PluginsScreen(
) { ) {
when { when {
state.isLoading && state.plugins.isEmpty() -> { state.isLoading && state.plugins.isEmpty() -> {
LoadingState(modifier = Modifier.padding(paddingValues)) SkeletonListState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -46,8 +46,8 @@ import com.m57.hermescontrol.theme.LocalHermesStatusColors
import com.m57.hermescontrol.ui.common.EmptyState 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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.ToastEffect
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
@@ -85,7 +85,7 @@ fun ProcessesScreen(
} }
state.isLoading && state.processes.isEmpty() -> { state.isLoading && state.processes.isEmpty() -> {
LoadingState() SkeletonListState()
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -54,8 +54,8 @@ import com.m57.hermescontrol.R
import com.m57.hermescontrol.ui.common.EmptyState 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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.ToastEffect
import com.m57.hermescontrol.ui.profiles.components.ProfileBuilderView import com.m57.hermescontrol.ui.profiles.components.ProfileBuilderView
import com.m57.hermescontrol.ui.profiles.components.validateProfileName import com.m57.hermescontrol.ui.profiles.components.validateProfileName
@@ -134,7 +134,7 @@ fun ProfilesScreen(
} else { } else {
when { when {
state.isLoading && state.profiles.isEmpty() -> { state.isLoading && state.profiles.isEmpty() -> {
LoadingState(modifier = Modifier.padding(paddingValues)) SkeletonListState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -52,8 +52,8 @@ import com.m57.hermescontrol.theme.LocalHermesStatusColors
import com.m57.hermescontrol.ui.common.EmptyState 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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.ToastEffect
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
@@ -102,10 +102,7 @@ fun ProvidersScreen(
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading && state.providers.isEmpty() -> { state.isLoading && state.providers.isEmpty() -> {
LoadingState( SkeletonListState(modifier = Modifier.padding(paddingValues))
subtitle = stringResource(R.string.loading_state_subtitle_providers),
modifier = Modifier.padding(paddingValues),
)
} }
state.errorMessage != null && state.providers.isEmpty() -> { state.errorMessage != null && state.providers.isEmpty() -> {
@@ -90,9 +90,9 @@ import com.m57.hermescontrol.theme.LocalSpacing
import com.m57.hermescontrol.ui.common.EmptyState 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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SearchBar import com.m57.hermescontrol.ui.common.SearchBar
import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.StatCard import com.m57.hermescontrol.ui.common.StatCard
import com.m57.hermescontrol.ui.common.StatusBadge import com.m57.hermescontrol.ui.common.StatusBadge
import com.m57.hermescontrol.ui.common.StatusBadgeType import com.m57.hermescontrol.ui.common.StatusBadgeType
@@ -491,7 +491,7 @@ fun SessionsScreen(
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
when { when {
state.isSearching && state.searchResults.isEmpty() -> { state.isSearching && state.searchResults.isEmpty() -> {
LoadingState() SkeletonListState()
} }
state.searchError != null -> { state.searchError != null -> {
@@ -579,7 +579,7 @@ fun SessionsScreen(
} }
state.isLoading && state.sessions.isEmpty() -> { state.isLoading && state.sessions.isEmpty() -> {
LoadingState() SkeletonListState()
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -74,6 +74,7 @@ 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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SearchBar import com.m57.hermescontrol.ui.common.SearchBar
import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.ToastEffect
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
@@ -317,7 +318,7 @@ private fun InstalledSkillsView(
when { when {
state.isLoading -> { state.isLoading -> {
LoadingState() SkeletonListState()
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -587,7 +588,7 @@ private fun HubBrowseView(
when { when {
state.isHubSearching -> { state.isHubSearching -> {
LoadingState() SkeletonListState()
} }
state.hubSearchError != null -> { state.hubSearchError != null -> {
@@ -48,7 +48,7 @@ import com.m57.hermescontrol.ui.common.DetailDialog
import com.m57.hermescontrol.ui.common.DetailRow import com.m57.hermescontrol.ui.common.DetailRow
import com.m57.hermescontrol.ui.common.EmptyState 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.LoadingState import com.m57.hermescontrol.ui.common.SkeletonListState
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
import com.m57.hermescontrol.ui.skills.SkillsUiState import com.m57.hermescontrol.ui.skills.SkillsUiState
@@ -117,7 +117,7 @@ internal fun HubBrowseView(
when { when {
state.isHubSearching -> { state.isHubSearching -> {
LoadingState() SkeletonListState()
} }
state.hubSearchError != null -> { state.hubSearchError != null -> {
@@ -54,6 +54,7 @@ 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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SearchBar import com.m57.hermescontrol.ui.common.SearchBar
import com.m57.hermescontrol.ui.common.SkeletonListState
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
import com.m57.hermescontrol.ui.common.toDetailRows import com.m57.hermescontrol.ui.common.toDetailRows
@@ -177,7 +178,7 @@ internal fun InstalledSkillsView(
when { when {
state.isLoading -> { state.isLoading -> {
LoadingState() SkeletonListState()
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -78,9 +78,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.InfoRow import com.m57.hermescontrol.ui.common.InfoRow
import com.m57.hermescontrol.ui.common.LoadingState
import com.m57.hermescontrol.ui.common.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SectionHeader import com.m57.hermescontrol.ui.common.SectionHeader
import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.StatCard import com.m57.hermescontrol.ui.common.StatCard
import com.m57.hermescontrol.ui.common.StatusBadge import com.m57.hermescontrol.ui.common.StatusBadge
import com.m57.hermescontrol.ui.common.StatusBadgeType import com.m57.hermescontrol.ui.common.StatusBadgeType
@@ -252,7 +252,7 @@ fun SystemScreen(
when { when {
state.isLoading && state.stats == null && state.doctorReport == null && state.isLoading && state.stats == null && state.doctorReport == null &&
state.status == null -> { state.status == null -> {
LoadingState() SkeletonListState()
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -44,9 +44,9 @@ import com.m57.hermescontrol.ui.common.DetailDialog
import com.m57.hermescontrol.ui.common.EmptyState 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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SearchBar import com.m57.hermescontrol.ui.common.SearchBar
import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.ToastEffect
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
@@ -87,7 +87,7 @@ fun ToolsetsScreen(
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading && state.toolsets.isEmpty() -> { state.isLoading && state.toolsets.isEmpty() -> {
LoadingState(modifier = Modifier.padding(paddingValues)) SkeletonListState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null -> { state.errorMessage != null -> {
@@ -53,9 +53,9 @@ import com.m57.hermescontrol.ui.common.DetailDialog
import com.m57.hermescontrol.ui.common.EmptyState 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.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SearchBar import com.m57.hermescontrol.ui.common.SearchBar
import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.StatusBadge import com.m57.hermescontrol.ui.common.StatusBadge
import com.m57.hermescontrol.ui.common.StatusBadgeType import com.m57.hermescontrol.ui.common.StatusBadgeType
import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.ToastEffect
@@ -244,7 +244,7 @@ fun WebhooksScreen(
) { paddingValues -> ) { paddingValues ->
when { when {
state.isLoading && state.baseUrl == null -> { state.isLoading && state.baseUrl == null -> {
LoadingState(modifier = Modifier.padding(paddingValues)) SkeletonListState(modifier = Modifier.padding(paddingValues))
} }
state.errorMessage != null && state.baseUrl == null -> { state.errorMessage != null && state.baseUrl == null -> {