Fix UX issues across all screens
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:
parent
19d8899057
commit
99bd1bff42
5 changed files with 121 additions and 25 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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") }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in a new issue