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 cc79bb43..ede67f7c 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 @@ -1,98 +1,59 @@ package com.m57.hermescontrol.ui.mcp -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.ContentCopy -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Key -import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Science -import androidx.compose.material.icons.filled.Storage import androidx.compose.material.icons.filled.UploadFile -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.FilterChip -import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.SecondaryTabRow -import androidx.compose.material3.Surface -import androidx.compose.material3.Switch -import androidx.compose.material3.Tab +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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.Modifier -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.m57.hermescontrol.R import com.m57.hermescontrol.data.local.AuthManager -import com.m57.hermescontrol.data.model.McpCatalogEntry import com.m57.hermescontrol.data.model.McpServer import com.m57.hermescontrol.theme.LocalSpacing -import com.m57.hermescontrol.ui.common.DetailDialog import com.m57.hermescontrol.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.NavIcon 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.StatusBadgeType import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.listContentPadding import com.m57.hermescontrol.ui.common.listItemSpacing -import com.m57.hermescontrol.ui.common.toDetailRows import com.m57.hermescontrol.ui.mcp.components.AddServerSection -import com.m57.hermescontrol.ui.mcp.components.CatalogSection +import com.m57.hermescontrol.ui.mcp.components.CatalogView import com.m57.hermescontrol.ui.mcp.components.McpDialogs import com.m57.hermescontrol.ui.mcp.components.ServerCard +@OptIn(ExperimentalMaterial3Api::class) @Composable fun McpServersScreen( modifier: Modifier = Modifier, @@ -138,120 +99,158 @@ fun McpServersScreen( HermesScaffold( title = { Text(stringResource(R.string.screen_mcp_servers)) }, navigationIcon = onOpenDrawer?.let { NavIcon.Menu(it) }, - isRefreshing = state.isLoading, - onRefresh = { viewModel.loadServers(forceRefresh = true) }, - actions = { - IconButton( - onClick = { viewModel.testAllServers() }, - enabled = !state.isTestingAll && state.servers.any { it.enabled }, - ) { - if (state.isTestingAll) { - CircularProgressIndicator( - modifier = Modifier.size(18.dp), - strokeWidth = 2.dp, - ) - } else { - Icon( - imageVector = Icons.Filled.Science, - contentDescription = stringResource(R.string.mcp_servers_action_test_all), - ) - } - } - IconButton( - onClick = { viewModel.toggleImportDialog() }, - ) { - Icon( - imageVector = Icons.Filled.UploadFile, - contentDescription = stringResource(R.string.mcp_servers_action_import_json), - ) + isRefreshing = if (state.selectedTab == McpTab.INSTALLED) state.isLoading else state.catalogLoading, + onRefresh = { + if (state.selectedTab == McpTab.INSTALLED) { + viewModel.loadServers(forceRefresh = true) + } else { + viewModel.loadCatalog() } }, - ) { paddingValues -> - when { - state.isLoading && state.servers.isEmpty() -> { - SkeletonListState(modifier = Modifier.padding(paddingValues)) - } - - state.errorMessage != null -> { - ErrorState( - message = state.errorMessage ?: "", - onRetry = { viewModel.loadServers() }, - modifier = Modifier.padding(paddingValues), - ) - } - - else -> { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = listContentPadding, - verticalArrangement = listItemSpacing, + actions = { + if (state.selectedTab == McpTab.INSTALLED) { + IconButton(onClick = { viewModel.toggleAddForm() }) { + Icon( + imageVector = if (state.showAddForm) Icons.Filled.Close else Icons.Filled.Add, + contentDescription = stringResource(R.string.mcp_servers_add_server), + ) + } + IconButton(onClick = { viewModel.toggleImportDialog() }) { + Icon( + imageVector = Icons.Filled.UploadFile, + contentDescription = stringResource(R.string.mcp_servers_action_import_json), + ) + } + IconButton( + onClick = { viewModel.testAllServers() }, + enabled = !state.isTestingAll && state.servers.any { it.enabled }, ) { - // ── 1. Add server ──────────────────────────────────── - item(key = "add-header") { - AddServerSection(state, viewModel, spacing) - } - - // ── 2. Search ──────────────────────────────────────── - item(key = "search") { - SearchBar( - query = query, - onQueryChange = { query = it }, - placeholder = stringResource(R.string.mcp_search_placeholder), + if (state.isTestingAll) { + CircularProgressIndicator( + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp, + ) + } else { + Icon( + imageVector = Icons.Filled.Science, + contentDescription = stringResource(R.string.mcp_servers_action_test_all), ) } + } + } + }, + ) { _ -> + Column(modifier = Modifier.fillMaxSize()) { + SingleChoiceSegmentedButtonRow( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + ) { + SegmentedButton( + selected = state.selectedTab == McpTab.INSTALLED, + onClick = { viewModel.setTab(McpTab.INSTALLED) }, + shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2), + ) { + Text(stringResource(R.string.mcp_tab_installed)) + } + SegmentedButton( + selected = state.selectedTab == McpTab.CATALOG, + onClick = { viewModel.setTab(McpTab.CATALOG) }, + shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2), + ) { + Text(stringResource(R.string.mcp_tab_catalog)) + } + } - // ── 3. Server list ─────────────────────────────────── - if (filteredServers.isEmpty() && query.isEmpty()) { - item(key = "empty") { - EmptyState( - title = stringResource(R.string.mcp_servers_empty_title), - subtitle = stringResource(R.string.mcp_servers_empty_desc), - actionLabel = stringResource(R.string.empty_action_add_server), - onAction = { viewModel.toggleAddForm() }, + when (state.selectedTab) { + McpTab.INSTALLED -> { + when { + state.isLoading && state.servers.isEmpty() -> { + SkeletonListState() + } + + state.errorMessage != null && state.servers.isEmpty() -> { + ErrorState( + message = state.errorMessage ?: "", + onRetry = { viewModel.loadServers() }, ) } - } else if (filteredServers.isEmpty()) { - item(key = "no-match") { - Text( - text = stringResource(R.string.mcp_no_match, query), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(spacing.md), - ) - } - } - items(filteredServers, key = { "server:${it.name}" }) { server -> - ServerCard( - server = server, - state = state, - viewModel = viewModel, - spacing = spacing, - onClick = { showDetail = server }, - onOpenBrowser = { url -> - try { - context.startActivity( - android.content.Intent( - android.content.Intent.ACTION_VIEW, - android.net.Uri.parse(url), - ), - ) - } catch (_: Exception) { + else -> { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = listContentPadding, + verticalArrangement = listItemSpacing, + ) { + item(key = "add-form") { + AddServerSection(state, viewModel, spacing) } - }, - ) - } - // ── 4. Catalog ────────────────────────────────────── - item(key = "catalog-header") { - CatalogSection( - state = state, - viewModel = viewModel, - spacing = spacing, - filteredCatalog = filteredCatalog, - ) + if (state.servers.isNotEmpty()) { + item(key = "search") { + SearchBar( + query = query, + onQueryChange = { query = it }, + placeholder = stringResource(R.string.mcp_search_placeholder), + ) + } + } + + if (state.servers.isEmpty()) { + item(key = "empty") { + EmptyState( + title = stringResource(R.string.mcp_servers_empty_title), + subtitle = stringResource(R.string.mcp_servers_empty_desc), + actionLabel = stringResource(R.string.mcp_tab_catalog), + onAction = { viewModel.setTab(McpTab.CATALOG) }, + modifier = Modifier.height(320.dp), + ) + } + } else if (filteredServers.isEmpty()) { + item(key = "no-match") { + Text( + text = stringResource(R.string.mcp_no_match, query), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(spacing.md), + ) + } + } + + items(filteredServers, key = { "server:${it.name}" }) { server -> + ServerCard( + server = server, + state = state, + viewModel = viewModel, + spacing = spacing, + onClick = { showDetail = server }, + onOpenBrowser = { url -> + try { + context.startActivity( + android.content.Intent( + android.content.Intent.ACTION_VIEW, + android.net.Uri.parse(url), + ), + ) + } catch (_: Exception) { + } + }, + ) + } + } + } } } + + McpTab.CATALOG -> { + CatalogView( + state = state, + viewModel = viewModel, + spacing = spacing, + filteredCatalog = filteredCatalog, + ) + } } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt index a83e0421..8232801b 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt @@ -32,7 +32,10 @@ import kotlinx.coroutines.withContext enum class AddServerMode { HTTP, Stdio } +enum class McpTab { INSTALLED, CATALOG } + data class McpServersUiState( + val selectedTab: McpTab = McpTab.INSTALLED, val isLoading: Boolean = false, val servers: List = emptyList(), val errorMessage: String? = null, @@ -547,6 +550,14 @@ class McpServersViewModel( } } + fun setTab(tab: McpTab) { + _uiState.update { it.copy(selectedTab = tab) } + val s = _uiState.value + if (tab == McpTab.CATALOG && s.catalogEntries.isEmpty() && !s.catalogLoading) { + loadCatalog() + } + } + fun updateCatalogQuery(v: String) { _uiState.update { it.copy(catalogQuery = v) } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/mcp/components/AddServerSection.kt b/app/src/main/java/com/m57/hermescontrol/ui/mcp/components/AddServerSection.kt index 4bae7bf1..3bc37626 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/mcp/components/AddServerSection.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/mcp/components/AddServerSection.kt @@ -5,29 +5,20 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.UploadFile import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.FilterChip -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SecondaryTabRow import androidx.compose.material3.Tab import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -50,37 +41,6 @@ fun AddServerSection( modifier: Modifier = Modifier, ) { Column(modifier = modifier) { - McpSectionHeader( - icon = Icons.Filled.Add, - title = stringResource(R.string.mcp_servers_add_server), - trailing = { - Row(horizontalArrangement = Arrangement.spacedBy(spacing.xs)) { - TextButton( - onClick = { viewModel.toggleImportDialog() }, - colors = - ButtonDefaults.textButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - contentColor = MaterialTheme.colorScheme.onSurface, - ), - ) { - Icon(Icons.Filled.UploadFile, contentDescription = null, modifier = Modifier.size(14.dp)) - Spacer(modifier = Modifier.width(spacing.xs)) - Text(stringResource(R.string.mcp_servers_action_import_json)) - } - TextButton( - onClick = { viewModel.toggleAddForm() }, - colors = - ButtonDefaults.textButtonColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer, - ), - ) { - Text(if (state.showAddForm) "Hide" else "New") - } - } - }, - ) - AnimatedVisibility(visible = state.showAddForm) { Card( modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/com/m57/hermescontrol/ui/mcp/components/CatalogSection.kt b/app/src/main/java/com/m57/hermescontrol/ui/mcp/components/CatalogSection.kt index 533123d2..2e2865d3 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/mcp/components/CatalogSection.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/mcp/components/CatalogSection.kt @@ -2,25 +2,21 @@ package com.m57.hermescontrol.ui.mcp.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Storage +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -34,91 +30,66 @@ import androidx.compose.ui.unit.dp import com.m57.hermescontrol.R import com.m57.hermescontrol.data.model.McpCatalogEntry import com.m57.hermescontrol.theme.Spacing +import com.m57.hermescontrol.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState +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.listItemSpacing import com.m57.hermescontrol.ui.mcp.McpServersUiState import com.m57.hermescontrol.ui.mcp.McpServersViewModel @Composable -fun CatalogSection( +fun CatalogView( state: McpServersUiState, viewModel: McpServersViewModel, spacing: Spacing, filteredCatalog: List, modifier: Modifier = Modifier, ) { - val catalogExpanded = remember { mutableStateOf(false) } - - Column(modifier = modifier) { - McpSectionHeader( - icon = Icons.Filled.Storage, - title = stringResource(R.string.mcp_servers_section_catalog), - trailing = { - TextButton( - onClick = { - catalogExpanded.value = !catalogExpanded.value - if (catalogExpanded.value && state.catalogEntries.isEmpty() && !state.catalogLoading) { - viewModel.loadCatalog() - } - }, - colors = - ButtonDefaults.textButtonColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer, - ), - ) { - Text(if (catalogExpanded.value) "Hide" else "Browse") - } - }, + Column(modifier = modifier.fillMaxSize()) { + SearchBar( + query = state.catalogQuery, + onQueryChange = viewModel::updateCatalogQuery, + placeholder = stringResource(R.string.mcp_servers_catalog_search), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), ) - AnimatedVisibility(visible = catalogExpanded.value) { - Column(modifier = Modifier.fillMaxWidth()) { - // Search - OutlinedTextField( - value = state.catalogQuery, - onValueChange = viewModel::updateCatalogQuery, - label = { Text(stringResource(R.string.mcp_servers_catalog_search)) }, - singleLine = true, - modifier = Modifier.fillMaxWidth().padding(bottom = spacing.sm), + when { + state.catalogLoading && state.catalogEntries.isEmpty() -> { + SkeletonListState() + } + + state.catalogError != null && state.catalogEntries.isEmpty() -> { + ErrorState( + message = state.catalogError, + onRetry = { viewModel.loadCatalog() }, ) + } - // Loading / error / content - when { - state.catalogLoading -> { - Spacer(modifier = Modifier.height(spacing.sm)) - Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { - CircularProgressIndicator(modifier = Modifier.size(24.dp)) - } - Spacer(modifier = Modifier.height(spacing.sm)) - } + filteredCatalog.isEmpty() -> { + EmptyState( + title = stringResource(R.string.mcp_servers_catalog_empty_title), + subtitle = stringResource(R.string.mcp_servers_catalog_empty), + actionLabel = stringResource(R.string.content_desc_refresh), + onAction = { viewModel.loadCatalog() }, + ) + } - state.catalogError != null -> { - ErrorState( - message = state.catalogError, - onRetry = { viewModel.loadCatalog() }, + else -> { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = listContentPadding, + verticalArrangement = listItemSpacing, + ) { + items(filteredCatalog, key = { "catalog:${it.name}" }) { entry -> + CatalogEntryCard( + entry = entry, + state = state, + viewModel = viewModel, + spacing = spacing, ) } - - filteredCatalog.isEmpty() -> { - Text( - text = stringResource(R.string.mcp_servers_catalog_empty), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(vertical = spacing.md), - ) - } - - else -> { - filteredCatalog.forEach { entry -> - CatalogEntryCard( - entry = entry, - state = state, - viewModel = viewModel, - spacing = spacing, - ) - Spacer(modifier = Modifier.height(spacing.sm)) - } - } } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/mcp/components/McpSectionHeader.kt b/app/src/main/java/com/m57/hermescontrol/ui/mcp/components/McpSectionHeader.kt deleted file mode 100644 index 9dd01b89..00000000 --- a/app/src/main/java/com/m57/hermescontrol/ui/mcp/components/McpSectionHeader.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.m57.hermescontrol.ui.mcp.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.m57.hermescontrol.theme.LocalSpacing - -@Composable -fun McpSectionHeader( - icon: ImageVector, - title: String, - modifier: Modifier = Modifier, - trailing: (@Composable () -> Unit)? = null, -) { - val spacing = LocalSpacing.current - Row( - modifier = - modifier - .fillMaxWidth() - .background( - color = MaterialTheme.colorScheme.surfaceContainerLow, - shape = RoundedCornerShape(12.dp), - ).padding(horizontal = spacing.md, vertical = spacing.sm), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.primary, - ) - Spacer(modifier = Modifier.width(spacing.sm)) - Text( - text = title, - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.Bold, - ) - } - trailing?.invoke() - } -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a057be79..b4023d78 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1840,4 +1840,7 @@ Reconnect No tools available Read-only policy restrictions + Installed + Catalog + No catalog entries