test
This commit is contained in:
449
app/src/main/java/com/example/scanwich/CaptureScreen.kt
Normal file
449
app/src/main/java/com/example/scanwich/CaptureScreen.kt
Normal file
@@ -0,0 +1,449 @@
|
||||
package com.example.scanwich
|
||||
|
||||
import android.Manifest
|
||||
import android.app.DatePickerDialog
|
||||
import android.app.TimePickerDialog
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import com.example.scanwich.FirebaseUtils.syncMealToFirestore
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CaptureScreen(dao: AppDao) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
var capturedBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||
var isAnalyzing by remember { mutableStateOf(false) }
|
||||
var currentMealData by remember { mutableStateOf<Triple<String, String, List<Int>>?>(null) }
|
||||
var mealDateTime by remember { mutableLongStateOf(System.currentTimeMillis()) }
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
var showBarcodeScanner by remember { mutableStateOf(false) }
|
||||
|
||||
var manualMealName by remember { mutableStateOf("") }
|
||||
var showFavoritesSheet by remember { mutableStateOf(false) }
|
||||
|
||||
val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicturePreview()) { bitmap ->
|
||||
if (bitmap != null) {
|
||||
capturedBitmap = bitmap
|
||||
mealDateTime = System.currentTimeMillis()
|
||||
analyzeImage(bitmap, null, { isAnalyzing = it }, { data, _ ->
|
||||
if (data != null) {
|
||||
currentMealData = data
|
||||
showBottomSheet = true
|
||||
} else {
|
||||
Toast.makeText(context, "Analyse échouée", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}, coroutineScope)
|
||||
}
|
||||
}
|
||||
|
||||
val permissionLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (isGranted) cameraLauncher.launch(null)
|
||||
else Toast.makeText(context, "Permission caméra requise", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
val barcodePermissionLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (isGranted) showBarcodeScanner = true
|
||||
else Toast.makeText(context, "Permission caméra requise pour le scan", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
val galleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
uri?.let {
|
||||
try {
|
||||
val inputStream = context.contentResolver.openInputStream(it)
|
||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||
capturedBitmap = bitmap
|
||||
|
||||
val exifStream = context.contentResolver.openInputStream(it)
|
||||
if (exifStream != null) {
|
||||
val exif = ExifInterface(exifStream)
|
||||
val dateStr = exif.getAttribute(ExifInterface.TAG_DATETIME)
|
||||
mealDateTime = if (dateStr != null) {
|
||||
SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.getDefault()).parse(dateStr)?.time ?: System.currentTimeMillis()
|
||||
} else {
|
||||
System.currentTimeMillis()
|
||||
}
|
||||
exifStream.close()
|
||||
}
|
||||
|
||||
analyzeImage(bitmap, null, { isAnalyzing = it }, { data, _ ->
|
||||
if (data != null) {
|
||||
currentMealData = data
|
||||
showBottomSheet = true
|
||||
} else {
|
||||
Toast.makeText(context, "L'IA n'a pas pu identifier le repas.", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}, coroutineScope)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(context, "Erreur lors du chargement : ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showBarcodeScanner) {
|
||||
BarcodeScannerDialog(
|
||||
onBarcodeScanned = { barcode ->
|
||||
showBarcodeScanner = false
|
||||
isAnalyzing = true
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val response = ApiClient.offApi.getProduct(barcode)
|
||||
if (response.status == 1 && response.product != null) {
|
||||
val p = response.product
|
||||
val nut = p.nutriments
|
||||
currentMealData = Triple(
|
||||
p.productName ?: "Produit inconnu",
|
||||
"Scanné via OpenFoodFacts",
|
||||
listOf(
|
||||
nut?.energyKcal?.toInt() ?: 0,
|
||||
nut?.carbs?.toInt() ?: 0,
|
||||
nut?.proteins?.toInt() ?: 0,
|
||||
nut?.fat?.toInt() ?: 0
|
||||
)
|
||||
)
|
||||
mealDateTime = System.currentTimeMillis()
|
||||
showBottomSheet = true
|
||||
} else {
|
||||
Toast.makeText(context, "Produit non trouvé", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(context, "Erreur API: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
} finally {
|
||||
isAnalyzing = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismiss = { showBarcodeScanner = false }
|
||||
)
|
||||
}
|
||||
|
||||
if (showFavoritesSheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showFavoritesSheet = false },
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
) {
|
||||
val favMeals by dao.getAllFavorites().collectAsState(initial = emptyList())
|
||||
Column(modifier = Modifier.padding(16.dp).fillMaxHeight(0.6f)) {
|
||||
Text("Mes Favoris", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
if (favMeals.isEmpty()) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("Aucun favori enregistré", color = Color.Gray)
|
||||
}
|
||||
} else {
|
||||
LazyColumn {
|
||||
items(favMeals) { fav ->
|
||||
ListItem(
|
||||
headlineContent = { Text(fav.name) },
|
||||
supportingContent = { Text("${fav.calories} kcal - G:${fav.carbs} P:${fav.protein} L:${fav.fat}") },
|
||||
trailingContent = {
|
||||
IconButton(onClick = {
|
||||
currentMealData = Triple(fav.name, fav.analysisText, listOf(fav.calories, fav.carbs, fav.protein, fav.fat))
|
||||
mealDateTime = System.currentTimeMillis()
|
||||
showFavoritesSheet = false
|
||||
showBottomSheet = true
|
||||
}) { Icon(Icons.Default.Add, null) }
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
currentMealData = Triple(fav.name, fav.analysisText, listOf(fav.calories, fav.carbs, fav.protein, fav.fat))
|
||||
mealDateTime = System.currentTimeMillis()
|
||||
showFavoritesSheet = false
|
||||
showBottomSheet = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showBottomSheet && currentMealData != null) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showBottomSheet = false },
|
||||
sheetState = sheetState,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier.fillMaxHeight(0.85f)
|
||||
) {
|
||||
var mealType by remember { mutableStateOf("Déjeuner") }
|
||||
val calendar = Calendar.getInstance().apply { timeInMillis = mealDateTime }
|
||||
|
||||
var editableName by remember { mutableStateOf(currentMealData!!.first) }
|
||||
var editableDesc by remember { mutableStateOf(currentMealData!!.second) }
|
||||
|
||||
val mealValues = currentMealData!!.third
|
||||
val editableCalories = mealValues[0].toString()
|
||||
val editableCarbs = mealValues[1].toString()
|
||||
val editableProtein = mealValues[2].toString()
|
||||
val editableFat = mealValues[3].toString()
|
||||
|
||||
LaunchedEffect(currentMealData) {
|
||||
editableName = currentMealData!!.first
|
||||
editableDesc = currentMealData!!.second
|
||||
}
|
||||
|
||||
Column(modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 32.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text("Résumé du repas", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = editableName,
|
||||
onValueChange = { editableName = it },
|
||||
label = { Text("Nom du repas") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(value = editableCalories, onValueChange = { }, label = { Text("Cal") }, modifier = Modifier.weight(1f), readOnly = true)
|
||||
OutlinedTextField(value = editableCarbs, onValueChange = { }, label = { Text("Glu") }, modifier = Modifier.weight(1f), readOnly = true)
|
||||
OutlinedTextField(value = editableProtein, onValueChange = { }, label = { Text("Pro") }, modifier = Modifier.weight(1f), readOnly = true)
|
||||
OutlinedTextField(value = editableFat, onValueChange = { }, label = { Text("Lip") }, modifier = Modifier.weight(1f), readOnly = true)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = editableDesc,
|
||||
onValueChange = { editableDesc = it },
|
||||
label = { Text("Description / Précisions pour l'IA") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = 3
|
||||
)
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(top = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = {
|
||||
analyzeImage(capturedBitmap, "$editableName : $editableDesc", { isAnalyzing = it }, { data, _ ->
|
||||
if (data != null) {
|
||||
currentMealData = data
|
||||
} else {
|
||||
Toast.makeText(context, "Analyse échouée", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}, coroutineScope)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary),
|
||||
enabled = !isAnalyzing
|
||||
) {
|
||||
Icon(Icons.Default.Refresh, null)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Ressoumettre")
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
dao.insertFavorite(FavoriteMeal(
|
||||
name = editableName,
|
||||
analysisText = editableDesc,
|
||||
calories = editableCalories.toIntOrNull() ?: 0,
|
||||
carbs = editableCarbs.toIntOrNull() ?: 0,
|
||||
protein = editableProtein.toIntOrNull() ?: 0,
|
||||
fat = editableFat.toIntOrNull() ?: 0
|
||||
))
|
||||
Toast.makeText(context, "Ajouté aux favoris !", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = !isAnalyzing
|
||||
) {
|
||||
Icon(Icons.Default.Favorite, null)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Favori")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text("Catégorie :", fontWeight = FontWeight.Bold)
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
listOf("Déjeuner", "Dîner", "Souper", "Collation").forEach { type ->
|
||||
FilterChip(selected = mealType == type, onClick = { mealType = type }, label = { Text(type) })
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Button(onClick = {
|
||||
DatePickerDialog(context, { _, y, m, d ->
|
||||
calendar.set(y, m, d)
|
||||
TimePickerDialog(context, { _, hh, mm ->
|
||||
calendar.set(Calendar.HOUR_OF_DAY, hh)
|
||||
calendar.set(Calendar.MINUTE, mm)
|
||||
mealDateTime = calendar.timeInMillis
|
||||
}, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), true).show()
|
||||
}, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)).show()
|
||||
}, modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer)) {
|
||||
Icon(Icons.Default.DateRange, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
val formattedDate = SimpleDateFormat("dd/MM HH:mm", Locale.getDefault()).format(Date(mealDateTime))
|
||||
Text("Date/Heure: $formattedDate")
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
val meal = Meal(
|
||||
date = mealDateTime,
|
||||
name = editableName,
|
||||
analysisText = editableDesc,
|
||||
totalCalories = editableCalories.toIntOrNull() ?: 0,
|
||||
carbs = editableCarbs.toIntOrNull() ?: 0,
|
||||
protein = editableProtein.toIntOrNull() ?: 0,
|
||||
fat = editableFat.toIntOrNull() ?: 0,
|
||||
type = mealType
|
||||
)
|
||||
dao.insertMeal(meal)
|
||||
syncMealToFirestore(meal) // Firestore Sync
|
||||
showBottomSheet = false
|
||||
capturedBitmap = null
|
||||
Toast.makeText(context, "Repas enregistré !", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||
enabled = !isAnalyzing
|
||||
) {
|
||||
Icon(Icons.Default.Check, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Confirmer et Enregistrer")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp).verticalScroll(rememberScrollState())) {
|
||||
Text("Scan-Wich Analysis", style = MaterialTheme.typography.headlineMedium)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(onClick = {
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
cameraLauncher.launch(null)
|
||||
} else {
|
||||
permissionLauncher.launch(Manifest.permission.CAMERA)
|
||||
}
|
||||
}, modifier = Modifier.weight(1f)) { Icon(Icons.Default.Add, null); Text(" Caméra") }
|
||||
|
||||
Button(onClick = {
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
showBarcodeScanner = true
|
||||
} else {
|
||||
barcodePermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||
}
|
||||
}, modifier = Modifier.weight(1f)) { Icon(Icons.Default.QrCodeScanner, null); Text(" Barcode") }
|
||||
|
||||
Button(onClick = { galleryLauncher.launch("image/*") }, modifier = Modifier.weight(1f)) { Icon(Icons.Default.Share, null); Text(" Galerie") }
|
||||
}
|
||||
|
||||
capturedBitmap?.let {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text("Image sélectionnée :", style = MaterialTheme.typography.labelMedium)
|
||||
Image(
|
||||
bitmap = it.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxWidth().height(250.dp).clip(MaterialTheme.shapes.medium).background(Color.Gray)
|
||||
)
|
||||
}
|
||||
|
||||
if (isAnalyzing) {
|
||||
Spacer(Modifier.height(32.dp))
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
|
||||
CircularProgressIndicator()
|
||||
Text("Analyse en cours...", modifier = Modifier.padding(top = 8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Button(
|
||||
onClick = { showFavoritesSheet = true },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer)
|
||||
) {
|
||||
Icon(Icons.Default.Favorite, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Utiliser un Favori")
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(32.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text("Analyse par texte", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
OutlinedTextField(
|
||||
value = manualMealName,
|
||||
onValueChange = { manualMealName = it },
|
||||
label = { Text("Qu'avez-vous mangé ?") },
|
||||
placeholder = { Text("ex: Un sandwich au poulet et une pomme") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(top = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = {
|
||||
analyzeImage(null, manualMealName, { isAnalyzing = it }, { data, _ ->
|
||||
if (data != null) {
|
||||
currentMealData = data
|
||||
showBottomSheet = true
|
||||
} else {
|
||||
Toast.makeText(context, "Erreur IA", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}, coroutineScope)
|
||||
},
|
||||
enabled = manualMealName.isNotBlank() && !isAnalyzing,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Icon(Icons.Default.Refresh, null)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Analyser via IA")
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
currentMealData = Triple(manualMealName, "Ajout manuel", listOf(0, 0, 0, 0))
|
||||
mealDateTime = System.currentTimeMillis()
|
||||
showBottomSheet = true
|
||||
},
|
||||
enabled = manualMealName.isNotBlank() && !isAnalyzing,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text("Direct (0 kcal)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user