Replace two-button image picker with bottom sheet chooser
Some checks failed
Android CI / build (push) Successful in 1m42s
Web CI / test (push) Has been cancelled

Single "Add photo" button opens a ModalBottomSheet with camera
and gallery options, matching standard mobile UX patterns.
Switches to material-icons-extended for CameraAlt/PhotoLibrary icons.
Button label changes to "Change photo" once an image is selected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-05-20 05:42:37 +02:00
parent 9f660d50bc
commit 19d8899057
2 changed files with 57 additions and 6 deletions

View file

@ -44,7 +44,7 @@ dependencies {
implementation platform('androidx.compose:compose-bom:2025.05.01')
implementation 'androidx.activity:activity-compose:1.10.1'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.compose.material:material-icons-core'
implementation 'androidx.compose.material:material-icons-extended'
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'

View file

@ -1,8 +1,13 @@
package com.danvics.calorieai.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CameraAlt
import androidx.compose.material.icons.filled.PhotoLibrary
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.danvics.calorieai.data.MealEntry
@ -13,6 +18,7 @@ import java.time.format.DateTimeFormatter
private val MEAL_TYPES = listOf("Breakfast", "Lunch", "Dinner", "Snack", "Other")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LogMealScreen(
editing: MealEntry?,
@ -31,6 +37,36 @@ fun LogMealScreen(
var description by remember(editing?.id) { mutableStateOf(editing?.description ?: "") }
var measure by remember(editing?.id) { mutableStateOf(editing?.measure ?: "") }
var estimate by remember(editing?.id) { mutableStateOf(editing?.estimate ?: NutritionEstimate()) }
var showImageSheet by remember { mutableStateOf(false) }
if (showImageSheet) {
ModalBottomSheet(onDismissRequest = { showImageSheet = false }) {
Column(Modifier.padding(bottom = 32.dp)) {
Text(
"Add photo",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
HorizontalDivider()
ListItem(
headlineContent = { Text("Take photo") },
leadingContent = { Icon(Icons.Default.CameraAlt, contentDescription = null) },
modifier = Modifier.clickable {
showImageSheet = false
onTakePhoto()
}
)
ListItem(
headlineContent = { Text("Choose from gallery") },
leadingContent = { Icon(Icons.Default.PhotoLibrary, contentDescription = null) },
modifier = Modifier.clickable {
showImageSheet = false
onPickImage()
}
)
}
}
}
SectionCard("Meal details") {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
@ -40,12 +76,27 @@ fun LogMealScreen(
DropdownField("Meal type", MEAL_TYPES, mealType, { mealType = it })
Field("Description", description, { description = it }, singleLine = false, placeholder = "e.g. grilled salmon, rice, broccoli")
Field("Portion or measure", measure, { measure = it }, placeholder = "e.g. one plate, 450g")
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
OutlinedButton(onClick = onTakePhoto, modifier = Modifier.weight(1f)) { Text("Take photo") }
OutlinedButton(onClick = onPickImage, modifier = Modifier.weight(1f)) { Text("Choose image") }
OutlinedButton(
onClick = { showImageSheet = true },
modifier = Modifier.fillMaxWidth()
) {
Icon(
if (selectedImageName.isNotBlank()) Icons.Default.CameraAlt else Icons.Default.PhotoLibrary,
contentDescription = null,
modifier = Modifier.size(18.dp).padding(end = 0.dp)
)
Spacer(Modifier.width(8.dp))
Text(if (selectedImageName.isNotBlank()) "Change photo" else "Add photo")
}
if (selectedImageName.isNotBlank()) {
Text("Image: $selectedImageName", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
selectedImageName,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.weight(1f)
)
}
}
Button(
enabled = !busy && (description.isNotBlank() || selectedImageName.isNotBlank()),
@ -56,7 +107,7 @@ fun LogMealScreen(
Text(
status,
style = MaterialTheme.typography.bodySmall,
color = if (status.startsWith("Analysis failed") || status.startsWith("Set API")) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary
color = if (status.startsWith("Analysis failed") || status.startsWith("No AI")) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary
)
}
}