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(null) } var isAnalyzing by remember { mutableStateOf(false) } var currentMealData by remember { mutableStateOf>?>(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)") } } } }