Fix UX issues across all screens
Some checks failed
Android CI / build (push) Successful in 1m59s
Web CI / test (push) Failing after 1m6s

ConnectScreen:
- Keyboard types: Uri for URL, Password for password
- IME chain: Next→Next→Done across all three fields
- Pressing Done on password submits the form
- Password visibility toggle (show/hide button)

PlansScreen:
- Delete plan now requires confirmation dialog
- Plan content bumped to bodyMedium (was too small)
- Plan history uses Card with primaryContainer highlight for active plan
- "Active" badge on selected plan instead of just bold text

SettingsScreen:
- "Profile saved" status auto-clears after 3 seconds

CalorieAiApp:
- Top bar title shows "Edit meal" when editing an existing entry

DiaryScreen:
- "Deselect" button next to "Trash" when items are selected

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-05-20 05:44:34 +02:00
parent 19d8899057
commit 99bd1bff42
5 changed files with 121 additions and 25 deletions

View file

@ -54,7 +54,10 @@ fun CalorieAiApp(
Scaffold(
topBar = {
TopAppBar(
title = { Text(screen.label, style = MaterialTheme.typography.titleLarge) },
title = {
val title = if (screen == Screen.Log && editing != null) "Edit meal" else screen.label
Text(title, style = MaterialTheme.typography.titleLarge)
},
colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.surface),
actions = {
if (syncing) {

View file

@ -1,12 +1,22 @@
package com.danvics.calorieai.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
@Composable
@ -14,6 +24,8 @@ fun ConnectScreen(error: String, connecting: Boolean, onConnect: (String, String
var serverUrl by remember { mutableStateOf("") }
var username by remember { mutableStateOf("admin") }
var password by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) }
val focusManager = LocalFocusManager.current
Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
@ -33,7 +45,9 @@ fun ConnectScreen(error: String, connecting: Boolean, onConnect: (String, String
label = { Text("Server URL") },
placeholder = { Text("http://192.168.1.10:3000") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) })
)
Spacer(Modifier.height(12.dp))
OutlinedTextField(
@ -41,7 +55,9 @@ fun ConnectScreen(error: String, connecting: Boolean, onConnect: (String, String
onValueChange = { username = it },
label = { Text("Username") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) })
)
Spacer(Modifier.height(12.dp))
OutlinedTextField(
@ -50,7 +66,21 @@ fun ConnectScreen(error: String, connecting: Boolean, onConnect: (String, String
label = { Text("Password") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
visualTransformation = PasswordVisualTransformation()
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = {
focusManager.clearFocus()
if (serverUrl.isNotBlank() && username.isNotBlank() && password.isNotBlank() && !connecting)
onConnect(serverUrl.trim(), username.trim(), password)
}),
trailingIcon = {
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(
if (passwordVisible) Icons.Default.VisibilityOff else Icons.Default.Visibility,
contentDescription = if (passwordVisible) "Hide password" else "Show password"
)
}
}
)
Spacer(Modifier.height(20.dp))
Button(

View file

@ -52,16 +52,17 @@ private fun EntriesContent(
Field("Search meals", query, { query = it }, placeholder = "Search by name, type, or food groups...")
if (selected.isNotEmpty()) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
Text(
"${selected.size} selected",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.align(Alignment.CenterVertically).weight(1f)
modifier = Modifier.weight(1f)
)
TextButton(onClick = { selected = emptySet() }) { Text("Deselect") }
Button(
onClick = { onMoveToTrash(selected); selected = emptySet() },
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
) { Text("Move to trash") }
) { Text("Trash") }
}
}

View file

@ -3,6 +3,7 @@ package com.danvics.calorieai.ui
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@ -22,6 +23,24 @@ fun PlansScreen(
) = Page {
val selectedPlan = plansState.plans.find { it.id == plansState.selectedPlanId } ?: plansState.plans.firstOrNull()
var tuneNote by remember { mutableStateOf("") }
var showDeleteConfirm by remember { mutableStateOf(false) }
if (showDeleteConfirm && selectedPlan != null) {
AlertDialog(
onDismissRequest = { showDeleteConfirm = false },
title = { Text("Delete plan?") },
text = { Text("\"${selectedPlan.title}\" will be permanently deleted.") },
confirmButton = {
TextButton(
onClick = { onDelete(selectedPlan.id); showDeleteConfirm = false },
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)
) { Text("Delete") }
},
dismissButton = {
TextButton(onClick = { showDeleteConfirm = false }) { Text("Cancel") }
}
)
}
SectionCard("Generate plan") {
Text(
@ -47,16 +66,25 @@ fun PlansScreen(
}
if (selectedPlan != null) {
SectionCard("${selectedPlan.title}") {
SectionCard(selectedPlan.title) {
Text(
"Version ${selectedPlan.version} · ${selectedPlan.createdAt.take(10)}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) {
Text(selectedPlan.content, style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(12.dp))
Text(
selectedPlan.content,
style = MaterialTheme.typography.bodyMedium,
lineHeight = MaterialTheme.typography.bodyMedium.lineHeight,
modifier = Modifier.padding(14.dp)
)
}
Field("Update notes", tuneNote, { tuneNote = it }, singleLine = false, placeholder = "e.g. walked 7k steps daily, felt hungry at night, lost 0.5 kg")
Field(
"Update notes", tuneNote, { tuneNote = it },
singleLine = false,
placeholder = "e.g. walked 7k steps daily, felt hungry at night, lost 0.5 kg"
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
enabled = !planBusy && tuneNote.isNotBlank(),
@ -64,7 +92,7 @@ fun PlansScreen(
modifier = Modifier.weight(1f)
) { Text(if (planBusy) "Updating..." else "Update plan") }
OutlinedButton(
onClick = { onDelete(selectedPlan.id) },
onClick = { showDeleteConfirm = true },
modifier = Modifier.weight(1f),
colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.error)
) { Text("Delete") }
@ -72,28 +100,54 @@ fun PlansScreen(
}
} else if (!planBusy) {
SectionCard("No plan yet") {
Text("Generate your first plan to get started.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(
"Generate your first plan to get started.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
if (plansState.plans.size > 1) {
SectionCard("Plan history") {
plansState.plans.forEach { plan ->
OutlinedButton(
val isSelected = plan.id == plansState.selectedPlanId
Card(
onClick = { onSelect(plan.id) },
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer
else MaterialTheme.colorScheme.surface
)
) {
Column(Modifier.fillMaxWidth()) {
Text(
plan.title,
fontWeight = if (plan.id == plansState.selectedPlanId) FontWeight.Bold else FontWeight.Normal,
style = MaterialTheme.typography.bodyMedium
)
Text(
"Version ${plan.version} · ${plan.createdAt.take(10)}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Row(
Modifier.padding(12.dp).fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Column(Modifier.weight(1f)) {
Text(
plan.title,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
style = MaterialTheme.typography.bodyMedium,
color = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer
else MaterialTheme.colorScheme.onSurface
)
Text(
"Version ${plan.version} · ${plan.createdAt.take(10)}",
style = MaterialTheme.typography.labelSmall,
color = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)
else MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (isSelected) {
Text(
"Active",
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
}
}

View file

@ -6,6 +6,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.danvics.calorieai.data.ServerSettings
import kotlinx.coroutines.delay
private val ACTIVITY_LEVELS = listOf("Low", "Moderate", "High")
private val PACE_OPTIONS = listOf("Gentle", "Steady", "Aggressive")
@ -20,6 +21,13 @@ fun SettingsScreen(
var draft by remember(settings) { mutableStateOf(settings) }
var status by remember { mutableStateOf("") }
LaunchedEffect(status) {
if (status.isNotBlank()) {
delay(3000)
status = ""
}
}
SectionCard("Profile") {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Field("Height (cm)", draft.heightCm, { draft = draft.copy(heightCm = it) }, modifier = Modifier.weight(1f), placeholder = "175")