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