Replace two-button image picker with bottom sheet chooser
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:
parent
9f660d50bc
commit
19d8899057
2 changed files with 57 additions and 6 deletions
|
|
@ -44,7 +44,7 @@ dependencies {
|
||||||
implementation platform('androidx.compose:compose-bom:2025.05.01')
|
implementation platform('androidx.compose:compose-bom:2025.05.01')
|
||||||
implementation 'androidx.activity:activity-compose:1.10.1'
|
implementation 'androidx.activity:activity-compose:1.10.1'
|
||||||
implementation 'androidx.compose.material3:material3'
|
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'
|
||||||
implementation 'androidx.compose.ui:ui-tooling-preview'
|
implementation 'androidx.compose.ui:ui-tooling-preview'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
package com.danvics.calorieai.ui
|
package com.danvics.calorieai.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
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.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.danvics.calorieai.data.MealEntry
|
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")
|
private val MEAL_TYPES = listOf("Breakfast", "Lunch", "Dinner", "Snack", "Other")
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LogMealScreen(
|
fun LogMealScreen(
|
||||||
editing: MealEntry?,
|
editing: MealEntry?,
|
||||||
|
|
@ -31,6 +37,36 @@ fun LogMealScreen(
|
||||||
var description by remember(editing?.id) { mutableStateOf(editing?.description ?: "") }
|
var description by remember(editing?.id) { mutableStateOf(editing?.description ?: "") }
|
||||||
var measure by remember(editing?.id) { mutableStateOf(editing?.measure ?: "") }
|
var measure by remember(editing?.id) { mutableStateOf(editing?.measure ?: "") }
|
||||||
var estimate by remember(editing?.id) { mutableStateOf(editing?.estimate ?: NutritionEstimate()) }
|
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") {
|
SectionCard("Meal details") {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
|
@ -40,12 +76,27 @@ fun LogMealScreen(
|
||||||
DropdownField("Meal type", MEAL_TYPES, mealType, { mealType = it })
|
DropdownField("Meal type", MEAL_TYPES, mealType, { mealType = it })
|
||||||
Field("Description", description, { description = it }, singleLine = false, placeholder = "e.g. grilled salmon, rice, broccoli")
|
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")
|
Field("Portion or measure", measure, { measure = it }, placeholder = "e.g. one plate, 450g")
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
|
OutlinedButton(
|
||||||
OutlinedButton(onClick = onTakePhoto, modifier = Modifier.weight(1f)) { Text("Take photo") }
|
onClick = { showImageSheet = true },
|
||||||
OutlinedButton(onClick = onPickImage, modifier = Modifier.weight(1f)) { Text("Choose image") }
|
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()) {
|
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(
|
Button(
|
||||||
enabled = !busy && (description.isNotBlank() || selectedImageName.isNotBlank()),
|
enabled = !busy && (description.isNotBlank() || selectedImageName.isNotBlank()),
|
||||||
|
|
@ -56,7 +107,7 @@ fun LogMealScreen(
|
||||||
Text(
|
Text(
|
||||||
status,
|
status,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue