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 c5f8df82..0578827d 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
@@ -73,7 +73,10 @@ fun ChannelsScreen(
) { paddingValues ->
when {
state.isLoading && state.platforms.isEmpty() -> {
- LoadingState(modifier = Modifier.padding(paddingValues))
+ LoadingState(
+ subtitle = stringResource(R.string.loading_state_subtitle_channels),
+ modifier = Modifier.padding(paddingValues),
+ )
}
state.errorMessage != null -> {
diff --git a/app/src/main/java/com/m57/hermescontrol/ui/common/StateViews.kt b/app/src/main/java/com/m57/hermescontrol/ui/common/StateViews.kt
index 31e32f2b..5a9af51c 100644
--- a/app/src/main/java/com/m57/hermescontrol/ui/common/StateViews.kt
+++ b/app/src/main/java/com/m57/hermescontrol/ui/common/StateViews.kt
@@ -35,7 +35,10 @@ import com.m57.hermescontrol.theme.LocalSpacing
// ── Loading ───────────────────────────────────────────────────────────
@Composable
-fun LoadingState(modifier: Modifier = Modifier) {
+fun LoadingState(
+ modifier: Modifier = Modifier,
+ subtitle: String? = null,
+) {
Box(
modifier =
modifier
@@ -43,14 +46,24 @@ fun LoadingState(modifier: Modifier = Modifier) {
.testTag("loading_state"),
contentAlignment = Alignment.Center,
) {
- CircularProgressIndicator(
- color = MaterialTheme.colorScheme.primary,
- strokeWidth = 3.dp,
- modifier =
- Modifier
- .size(40.dp)
- .testTag("loading_spinner"),
- )
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ CircularProgressIndicator(
+ color = MaterialTheme.colorScheme.primary,
+ strokeWidth = 3.dp,
+ modifier =
+ Modifier
+ .size(40.dp)
+ .testTag("loading_spinner"),
+ )
+ if (subtitle != null) {
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ text = subtitle,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ }
}
}
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 4f32dbec..8cdef792 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
@@ -3,7 +3,6 @@ package com.m57.hermescontrol.ui.model
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@@ -16,13 +15,10 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
-import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -83,7 +79,10 @@ fun ModelScreen(
) { paddingValues ->
when {
state.isLoading && state.providers.isEmpty() -> {
- LoadingState(modifier = Modifier.padding(paddingValues))
+ LoadingState(
+ subtitle = stringResource(R.string.loading_state_subtitle_models),
+ modifier = Modifier.padding(paddingValues),
+ )
}
state.errorMessage != null -> {
@@ -105,190 +104,171 @@ fun ModelScreen(
}
else -> {
- Box(Modifier.fillMaxSize()) {
- if (state.isLoading && state.providers.isEmpty()) {
- CircularProgressIndicator()
- } else if (state.errorMessage != null && state.providers.isEmpty()) {
- Column(
- modifier = Modifier.padding(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
- Spacer(modifier = Modifier.height(16.dp))
- IconButton(onClick = { viewModel.loadModelOptions() }) {
- Icon(
- imageVector = Icons.Filled.Refresh,
- contentDescription = stringResource(R.string.action_retry),
- )
- }
- }
- } else {
- LazyColumn(
- modifier = Modifier.fillMaxSize(),
- contentPadding = PaddingValues(16.dp),
- verticalArrangement = Arrangement.spacedBy(12.dp),
- ) {
- item {
- SearchBar(
- query = query,
- onQueryChange = { query = it },
- placeholder = "Search models...",
- )
- }
- items(filteredProviders, key = { it.slug }) { provider ->
- val isExpanded = expandedProviderSlug == provider.slug
- val isCurrent = provider.is_current == true
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ item {
+ SearchBar(
+ query = query,
+ onQueryChange = { query = it },
+ placeholder = "Search models...",
+ )
+ }
+ items(filteredProviders, key = { it.slug }) { provider ->
+ val isExpanded = expandedProviderSlug == provider.slug
+ val isCurrent = provider.is_current == true
- Card(
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = {
+ expandedProviderSlug = if (isExpanded) null else provider.slug
+ },
+ colors =
+ CardDefaults.cardColors(
+ containerColor =
+ if (isCurrent) {
+ MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.2f)
+ } else {
+ MaterialTheme.colorScheme.surfaceVariant
+ },
+ ),
+ ) {
+ Column(
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ ) {
+ Row(
modifier = Modifier.fillMaxWidth(),
- onClick = {
- expandedProviderSlug = if (isExpanded) null else provider.slug
- },
- colors =
- CardDefaults.cardColors(
- containerColor =
- if (isCurrent) {
- MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.2f)
- } else {
- MaterialTheme.colorScheme.surfaceVariant
- },
- ),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
) {
- Column(
- modifier =
- Modifier
- .fillMaxWidth()
- .padding(16.dp),
- ) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Column {
- Text(
- text = provider.name,
- style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.Bold,
- )
- Text(
- text = stringResource(R.string.model_label_slug, provider.slug),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- )
- }
+ Column {
+ Text(
+ text = provider.name,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ )
+ Text(
+ text = stringResource(R.string.model_label_slug, provider.slug),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
- if (isCurrent) {
- Text(
- text = stringResource(R.string.model_status_current),
- style = MaterialTheme.typography.labelMedium,
- color = MaterialTheme.colorScheme.primary,
- fontWeight = FontWeight.Bold,
- )
- }
- }
+ if (isCurrent) {
+ Text(
+ text = stringResource(R.string.model_status_current),
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.Bold,
+ )
+ }
+ }
- provider.warning?.let {
- if (it.isNotBlank()) {
- Spacer(modifier = Modifier.height(8.dp))
- Text(
- text = it,
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.error,
- )
- }
- }
+ provider.warning?.let {
+ if (it.isNotBlank()) {
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = it,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.error,
+ )
+ }
+ }
- AnimatedVisibility(visible = isExpanded) {
- Column(modifier = Modifier.padding(top = 16.dp)) {
- Text(
- text = stringResource(R.string.model_label_available),
- style = MaterialTheme.typography.titleSmall,
- fontWeight = FontWeight.SemiBold,
- modifier = Modifier.padding(bottom = 8.dp),
- )
+ AnimatedVisibility(visible = isExpanded) {
+ Column(modifier = Modifier.padding(top = 16.dp)) {
+ Text(
+ text = stringResource(R.string.model_label_available),
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = FontWeight.SemiBold,
+ modifier = Modifier.padding(bottom = 8.dp),
+ )
- val models = provider.models.orEmpty()
- if (models.isEmpty()) {
- Text(
- text = stringResource(R.string.model_no_models),
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- )
- } else {
- models.forEach { model ->
- val isActive =
- state.activeProfile?.provider == provider.slug &&
- state.activeProfile?.model == model
- Card(
- onClick = {
- viewModel.selectModel(provider.slug, model)
- },
- modifier =
- Modifier
- .fillMaxWidth()
- .padding(vertical = 4.dp),
- colors =
- CardDefaults.cardColors(
- containerColor =
- if (isActive) {
- MaterialTheme.colorScheme
- .primaryContainer
- .copy(
- alpha = 0.3f,
- )
- } else {
- MaterialTheme.colorScheme.surface
- },
- ),
- border =
- BorderStroke(
- width = 1.dp,
- color =
- if (isActive) {
- MaterialTheme.colorScheme.primary
- } else {
- MaterialTheme.colorScheme.outline.copy(
- alpha = 0.2f,
- )
- },
- ),
- ) {
- Row(
- modifier =
- Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 12.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(
- text = model,
- style = MaterialTheme.typography.bodyMedium,
- fontWeight =
- if (isActive) {
- FontWeight.Bold
- } else {
- FontWeight.Normal
- },
- color =
- if (isActive) {
- MaterialTheme.colorScheme.onPrimaryContainer
- } else {
- MaterialTheme.colorScheme.onSurface
- },
- )
+ val models = provider.models.orEmpty()
+ if (models.isEmpty()) {
+ Text(
+ text = stringResource(R.string.model_no_models),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ } else {
+ models.forEach { model ->
+ val isActive =
+ state.activeProfile?.provider == provider.slug &&
+ state.activeProfile?.model == model
+ Card(
+ onClick = {
+ viewModel.selectModel(provider.slug, model)
+ },
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp),
+ colors =
+ CardDefaults.cardColors(
+ containerColor =
if (isActive) {
- Icon(
- imageVector = Icons.Filled.Check,
- contentDescription =
- stringResource(
- R.string.content_desc_active_model,
- ),
- tint = MaterialTheme.colorScheme.primary,
+ MaterialTheme.colorScheme
+ .primaryContainer
+ .copy(
+ alpha = 0.3f,
+ )
+ } else {
+ MaterialTheme.colorScheme.surface
+ },
+ ),
+ border =
+ BorderStroke(
+ width = 1.dp,
+ color =
+ if (isActive) {
+ MaterialTheme.colorScheme.primary
+ } else {
+ MaterialTheme.colorScheme.outline.copy(
+ alpha = 0.2f,
)
- }
- }
+ },
+ ),
+ ) {
+ Row(
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ horizontalArrangement =
+ Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ text = model,
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight =
+ if (isActive) {
+ FontWeight.Bold
+ } else {
+ FontWeight.Normal
+ },
+ color =
+ if (isActive) {
+ MaterialTheme.colorScheme.onPrimaryContainer
+ } else {
+ MaterialTheme.colorScheme.onSurface
+ },
+ )
+ if (isActive) {
+ Icon(
+ imageVector = Icons.Filled.Check,
+ contentDescription =
+ stringResource(
+ R.string.content_desc_active_model,
+ ),
+ tint = MaterialTheme.colorScheme.primary,
+ )
}
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2a6a522f..0fb2b2ec 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -397,4 +397,9 @@
Control panel for Hermes Agent
Auth Login
Pairing Login
+
+
+ Loading…
+ Fetching model providers…
+ Loading channels…
\ No newline at end of file