diff --git a/app/src/main/java/com/danvics/calorieai/ui/CalorieAiApp.kt b/app/src/main/java/com/danvics/calorieai/ui/CalorieAiApp.kt index 3e95c7a..33afe70 100644 --- a/app/src/main/java/com/danvics/calorieai/ui/CalorieAiApp.kt +++ b/app/src/main/java/com/danvics/calorieai/ui/CalorieAiApp.kt @@ -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) { diff --git a/app/src/main/java/com/danvics/calorieai/ui/ConnectScreen.kt b/app/src/main/java/com/danvics/calorieai/ui/ConnectScreen.kt index 9556fa2..d16c626 100644 --- a/app/src/main/java/com/danvics/calorieai/ui/ConnectScreen.kt +++ b/app/src/main/java/com/danvics/calorieai/ui/ConnectScreen.kt @@ -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( diff --git a/app/src/main/java/com/danvics/calorieai/ui/DiaryScreen.kt b/app/src/main/java/com/danvics/calorieai/ui/DiaryScreen.kt index 947be54..b1d842b 100644 --- a/app/src/main/java/com/danvics/calorieai/ui/DiaryScreen.kt +++ b/app/src/main/java/com/danvics/calorieai/ui/DiaryScreen.kt @@ -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") } } } diff --git a/app/src/main/java/com/danvics/calorieai/ui/PlansScreen.kt b/app/src/main/java/com/danvics/calorieai/ui/PlansScreen.kt index 670daee..1164bde 100644 --- a/app/src/main/java/com/danvics/calorieai/ui/PlansScreen.kt +++ b/app/src/main/java/com/danvics/calorieai/ui/PlansScreen.kt @@ -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 + ) + } } } } diff --git a/app/src/main/java/com/danvics/calorieai/ui/SettingsScreen.kt b/app/src/main/java/com/danvics/calorieai/ui/SettingsScreen.kt index 2909902..8014859 100644 --- a/app/src/main/java/com/danvics/calorieai/ui/SettingsScreen.kt +++ b/app/src/main/java/com/danvics/calorieai/ui/SettingsScreen.kt @@ -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")