feat: v1.6.0 — fix service 500, search bar, modal redesign

- Fix 500 error: cast to string before preg_replace
- Search bar to filter services by name/description
- Redesigned service action modal with status badge + colored buttons
- Version 1.6.0
This commit is contained in:
2026-06-09 19:20:18 -04:00
parent c4277c937f
commit 3e13ac60f1
4 changed files with 112 additions and 36 deletions

View File

@@ -12,8 +12,8 @@ android {
applicationId = "com.devlab.app"
minSdk = 26
targetSdk = 37
versionCode = 6
versionName = "1.5.0"
versionCode = 7
versionName = "1.6.0"
}
buildTypes {

View File

@@ -338,6 +338,15 @@ private fun ConsoleTab(state: ServerDetailUiState, viewModel: ServerDetailViewMo
private fun ServicesTab(state: ServerDetailUiState, viewModel: ServerDetailViewModel) {
var showServiceDialog by remember { mutableStateOf(false) }
var selectedService by remember { mutableStateOf<ServiceInfo?>(null) }
var serviceQuery by remember { mutableStateOf("") }
val filtered = remember(state.services, serviceQuery) {
if (serviceQuery.isBlank()) state.services
else state.services.filter {
it.name.contains(serviceQuery, ignoreCase = true) ||
it.description.contains(serviceQuery, ignoreCase = true)
}
}
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -351,7 +360,17 @@ private fun ServicesTab(state: ServerDetailUiState, viewModel: ServerDetailViewM
Text("Refresh")
}
}
Spacer(modifier = Modifier.height(12.dp))
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = serviceQuery,
onValueChange = { serviceQuery = it },
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Search services...") },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
singleLine = true
)
Spacer(modifier = Modifier.height(8.dp))
if (state.serviceActionResult != null) {
Card(
@@ -387,14 +406,14 @@ private fun ServicesTab(state: ServerDetailUiState, viewModel: ServerDetailViewM
}
}
}
state.services.isEmpty() -> {
filtered.isEmpty() -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("No services found", color = Grey500)
Text(if (serviceQuery.isNotBlank()) "No services match your search" else "No services found", color = Grey500)
}
}
else -> {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
state.services.forEach { service ->
filtered.forEach { service ->
val isActive = service.status == "active"
val isRunning = state.serviceActionRunning == "${service.name}:start" ||
state.serviceActionRunning == "${service.name}:stop" ||
@@ -446,33 +465,82 @@ private fun ServicesTab(state: ServerDetailUiState, viewModel: ServerDetailViewM
if (showServiceDialog && selectedService != null) {
val service = selectedService!!
val isActive = service.status == "active"
AlertDialog(
onDismissRequest = { showServiceDialog = false },
title = { Text(service.name, fontWeight = FontWeight.SemiBold) },
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Default.MiscellaneousServices,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(22.dp)
)
Spacer(modifier = Modifier.width(10.dp))
Text(text = service.name, fontWeight = FontWeight.SemiBold)
}
},
text = {
Column {
Text("Status: ${if (isActive) "Active" else service.status}", style = MaterialTheme.typography.bodyMedium)
Spacer(modifier = Modifier.height(16.dp))
Text("Choose an action:", style = MaterialTheme.typography.bodySmall, color = Grey500)
Surface(
color = (if (isActive) Green500 else Grey500).copy(alpha = 0.15f),
shape = RoundedCornerShape(8.dp)
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(8.dp)
.clip(androidx.compose.foundation.shape.CircleShape)
.background(if (isActive) Green500 else Grey500)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = if (isActive) "Active" else service.status.replaceFirstChar { it.uppercase() },
color = if (isActive) Green500 else Grey500,
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Medium
)
}
}
Spacer(modifier = Modifier.height(20.dp))
Text(
text = "Actions",
style = MaterialTheme.typography.labelMedium,
color = Grey500
)
Spacer(modifier = Modifier.height(8.dp))
if (isActive) {
ServiceActionChip("Restart", Icons.Default.Refresh, Orange500) {
viewModel.serviceAction(service.name, "restart")
showServiceDialog = false
}
ServiceActionChip("Stop", Icons.Default.Stop, Red500) {
viewModel.serviceAction(service.name, "stop")
showServiceDialog = false
}
ServiceActionChip("Reload", Icons.Default.Autorenew, Blue700) {
viewModel.serviceAction(service.name, "reload")
showServiceDialog = false
}
ServiceActionBtn(
label = "Restart",
icon = Icons.Default.Refresh,
color = Orange500,
onClick = { viewModel.serviceAction(service.name, "restart"); showServiceDialog = false }
)
ServiceActionBtn(
label = "Stop",
icon = Icons.Default.Stop,
color = Red500,
onClick = { viewModel.serviceAction(service.name, "stop"); showServiceDialog = false }
)
ServiceActionBtn(
label = "Reload",
icon = Icons.Default.Autorenew,
color = Blue700,
onClick = { viewModel.serviceAction(service.name, "reload"); showServiceDialog = false }
)
} else {
ServiceActionChip("Start", Icons.Default.PlayArrow, Green500) {
viewModel.serviceAction(service.name, "start")
showServiceDialog = false
}
ServiceActionBtn(
label = "Start",
icon = Icons.Default.PlayArrow,
color = Green500,
onClick = { viewModel.serviceAction(service.name, "start"); showServiceDialog = false }
)
}
}
},
@@ -480,21 +548,26 @@ private fun ServicesTab(state: ServerDetailUiState, viewModel: ServerDetailViewM
TextButton(onClick = { showServiceDialog = false }) {
Text("Cancel")
}
}
},
containerColor = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(16.dp)
)
}
}
@Composable
private fun ServiceActionChip(label: String, icon: androidx.compose.ui.graphics.vector.ImageVector, color: androidx.compose.ui.graphics.Color, onClick: () -> Unit) {
OutlinedButton(
private fun ServiceActionBtn(label: String, icon: androidx.compose.ui.graphics.vector.ImageVector, color: androidx.compose.ui.graphics.Color, onClick: () -> Unit) {
FilledTonalButton(
onClick = onClick,
modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp),
colors = ButtonDefaults.outlinedButtonColors(contentColor = color)
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = color.copy(alpha = 0.12f),
contentColor = color
)
) {
Icon(icon, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(modifier = Modifier.width(8.dp))
Text(label, fontWeight = FontWeight.Medium)
Text(label, fontWeight = FontWeight.SemiBold)
}
}