diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..e9b8783 --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,47 @@ +{ + "project_info": { + "project_number": "652626507041", + "project_id": "scan-wich", + "storage_bucket": "scan-wich.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:652626507041:android:e7a106785fe96df5137a60", + "android_client_info": { + "package_name": "com.example.scanwich" + } + }, + "oauth_client": [ + { + "client_id": "652626507041-1uesa4utc6r27gc3o10r4o2h8dhld7c2.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.example.scanwich", + "certificate_hash": "2c39e4131dcac8a1d4257b804718ac113f855b04" + } + }, + { + "client_id": "652626507041-5n42q37adh1guuv9gibfcf5uvekgunbe.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCHa7wOT-HR0RUQ-U_F0RE7ugY-1NpLPsM" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "652626507041-5n42q37adh1guuv9gibfcf5uvekgunbe.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..58dc526 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/example/scanwich/Database.kt b/app/src/main/java/com/example/scanwich/Database.kt index aeb77ce..42db2e1 100644 --- a/app/src/main/java/com/example/scanwich/Database.kt +++ b/app/src/main/java/com/example/scanwich/Database.kt @@ -11,6 +11,9 @@ data class Meal( val name: String = "Repas", val analysisText: String, val totalCalories: Int, + val carbs: Int = 0, + val protein: Int = 0, + val fat: Int = 0, val type: String = "Collation" ) @@ -53,7 +56,7 @@ interface AppDao { fun getSportsForDay(startOfDay: Long, endOfDay: Long): Flow> } -@Database(entities = [Meal::class, Glycemia::class, SportActivity::class], version = 5) +@Database(entities = [Meal::class, Glycemia::class, SportActivity::class], version = 6) abstract class AppDatabase : RoomDatabase() { abstract fun appDao(): AppDao companion object { diff --git a/app/src/main/java/com/example/scanwich/MainActivity.kt b/app/src/main/java/com/example/scanwich/MainActivity.kt index 02fe07a..bd752fe 100644 --- a/app/src/main/java/com/example/scanwich/MainActivity.kt +++ b/app/src/main/java/com/example/scanwich/MainActivity.kt @@ -37,6 +37,7 @@ 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.StrokeCap import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight @@ -44,7 +45,6 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.core.content.edit -import androidx.core.net.toUri import androidx.exifinterface.media.ExifInterface import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -53,6 +53,7 @@ import com.example.scanwich.ui.theme.ScanwichTheme import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.android.gms.common.api.ApiException +import com.google.gson.annotations.SerializedName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import okhttp3.OkHttpClient @@ -75,7 +76,7 @@ data class N8nMealRequest( ) interface N8nApi { - @POST("webhook/v1/gemini-proxy") + @POST("webhook-test/v1/gemini-proxy") suspend fun analyzeMeal( @Header("X-API-KEY") apiKey: String, @Body request: N8nMealRequest @@ -128,9 +129,6 @@ interface StravaApi { ): StravaTokenResponse } -// Annotation for Gson -annotation class SerializedName(val value: String) - // Helpers object ApiClient { private val loggingInterceptor = HttpLoggingInterceptor().apply { @@ -514,11 +512,16 @@ fun SetupScreen(prefs: SharedPreferences, onComplete: () -> Unit) { var targetCals = (bmr * multiplier).toInt() if (goal == "Perdre du poids") targetCals -= 500 + val targetCarbs = (targetCals * 0.5 / 4).toInt() + val targetProtein = (targetCals * 0.2 / 4).toInt() + val targetFat = (targetCals * 0.3 / 9).toInt() prefs.edit { putString("target_calories", targetCals.toString()) putString("target_carbs", targetCarbs.toString()) + putString("target_protein", targetProtein.toString()) + putString("target_fat", targetFat.toString()) putString("weight_kg", weightKg.toString()) putString("weight_display", weightDisplay) putBoolean("is_lbs", isLbs) @@ -539,6 +542,7 @@ fun SetupScreen(prefs: SharedPreferences, onComplete: () -> Unit) { } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun CaptureScreen(dao: AppDao, prefs: SharedPreferences, isDiabetic: Boolean) { val coroutineScope = rememberCoroutineScope() @@ -546,9 +550,10 @@ fun CaptureScreen(dao: AppDao, prefs: SharedPreferences, isDiabetic: Boolean) { var capturedBitmap by remember { mutableStateOf(null) } var isAnalyzing by remember { mutableStateOf(false) } - var showMealDialog by remember { mutableStateOf(false) } - var currentMealData by remember { mutableStateOf?>(null) } + 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 manualMealName by remember { mutableStateOf("") } @@ -559,7 +564,7 @@ fun CaptureScreen(dao: AppDao, prefs: SharedPreferences, isDiabetic: Boolean) { analyzeImage(bitmap, null, { isAnalyzing = it }, { data, error -> if (data != null) { currentMealData = data - showMealDialog = true + showBottomSheet = true } else { Toast.makeText(context, "Analyse échouée : $error", Toast.LENGTH_LONG).show() } @@ -600,7 +605,7 @@ fun CaptureScreen(dao: AppDao, prefs: SharedPreferences, isDiabetic: Boolean) { analyzeImage(bitmap, null, { isAnalyzing = it }, { data, error -> if (data != null) { currentMealData = data - showMealDialog = true + showBottomSheet = true } else { Toast.makeText(context, "L'IA n'a pas pu identifier le repas.", Toast.LENGTH_LONG).show() } @@ -611,84 +616,131 @@ fun CaptureScreen(dao: AppDao, prefs: SharedPreferences, isDiabetic: Boolean) { } } - if (showMealDialog && currentMealData != null) { - var mealType by remember { mutableStateOf("Déjeuner") } - val calendar = Calendar.getInstance().apply { timeInMillis = mealDateTime } - - var editableName by remember { mutableStateOf(currentMealData!!.first) } - var editableCalories by remember { mutableStateOf(currentMealData!!.third.toString()) } - var editableDesc by remember { mutableStateOf(currentMealData!!.second) } + 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 editableCalories = currentMealData!!.third[0].toString() + val editableCarbs = currentMealData!!.third[1].toString() + val editableProtein = currentMealData!!.third[2].toString() + val editableFat = currentMealData!!.third[3].toString() - AlertDialog( - onDismissRequest = { showMealDialog = false }, - title = { Text("Détails du repas") }, - text = { - // Ensure the content inside the dialog scrolls - Column(modifier = Modifier - .fillMaxWidth() - .heightIn(max = 450.dp) - .verticalScroll(rememberScrollState()) + // Update local state if currentMealData changes (e.g. after resubmission) + 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 + ) + + Button( + onClick = { + analyzeImage(capturedBitmap, "$editableName : $editableDesc", { isAnalyzing = it }, { data, error -> + if (data != null) { + currentMealData = data + } else { + Toast.makeText(context, "Analyse échouée : $error", Toast.LENGTH_LONG).show() + } + }, coroutineScope) + }, + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary), + enabled = !isAnalyzing ) { - OutlinedTextField( - value = editableName, - onValueChange = { editableName = it }, - label = { Text("Nom du repas") }, - modifier = Modifier.fillMaxWidth() - ) - OutlinedTextField( - value = editableCalories, - onValueChange = { editableCalories = it }, - label = { Text("Calories (kcal)") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth() - ) - OutlinedTextField( - value = editableDesc, - onValueChange = { editableDesc = it }, - label = { Text("Description") }, - modifier = Modifier.fillMaxWidth() - ) - - Spacer(Modifier.height(12.dp)) - Text("Type de repas :") + Icon(Icons.Default.Refresh, null) + Spacer(Modifier.width(8.dp)) + Text("Ressoumettre à l'IA") + } + + 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 -> - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { mealType = type }) { - RadioButton(selected = mealType == type, onClick = { mealType = type }) - Text(type) - } - } - Spacer(Modifier.height(8.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() - }) { - Text("Modifier Date/Heure: " + SimpleDateFormat("dd/MM HH:mm", Locale.getDefault()).format(Date(mealDateTime))) + FilterChip(selected = mealType == type, onClick = { mealType = type }, label = { Text(type) }) } } - }, - confirmButton = { + + Spacer(Modifier.height(16.dp)) Button(onClick = { - coroutineScope.launch { - dao.insertMeal(Meal( - date = mealDateTime, - name = editableName, - analysisText = editableDesc, - totalCalories = editableCalories.toIntOrNull() ?: 0, - type = mealType - )) - showMealDialog = false - capturedBitmap = null - Toast.makeText(context, "Repas enregistré !", Toast.LENGTH_SHORT).show() - } - }) { Text("Enregistrer") } + 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)) + Text("Date/Heure: " + SimpleDateFormat("dd/MM HH:mm", Locale.getDefault()).format(Date(mealDateTime))) + } + + Spacer(Modifier.height(24.dp)) + Button( + onClick = { + coroutineScope.launch { + dao.insertMeal(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 + )) + 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())) { @@ -742,7 +794,7 @@ fun CaptureScreen(dao: AppDao, prefs: SharedPreferences, isDiabetic: Boolean) { analyzeImage(null, manualMealName, { isAnalyzing = it }, { data, error -> if (data != null) { currentMealData = data - showMealDialog = true + showBottomSheet = true } else { Toast.makeText(context, "Erreur AI: $error", Toast.LENGTH_LONG).show() } @@ -758,9 +810,9 @@ fun CaptureScreen(dao: AppDao, prefs: SharedPreferences, isDiabetic: Boolean) { OutlinedButton( onClick = { - currentMealData = Triple(manualMealName, "Ajout manuel", 0) + currentMealData = Triple(manualMealName, "Ajout manuel", listOf(0, 0, 0, 0)) mealDateTime = System.currentTimeMillis() - showMealDialog = true + showBottomSheet = true }, enabled = manualMealName.isNotBlank() && !isAnalyzing, modifier = Modifier.weight(1f) @@ -771,6 +823,37 @@ fun CaptureScreen(dao: AppDao, prefs: SharedPreferences, isDiabetic: Boolean) { } } +@Composable +fun DailyGoalChart(label: String, current: Int, target: Int, color: Color) { + val progress = if (target > 0) current.toFloat() / target else 0f + Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.width(80.dp)) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.size(60.dp)) { + CircularProgressIndicator( + progress = { 1f }, + modifier = Modifier.fillMaxSize(), + color = color.copy(alpha = 0.2f), + strokeWidth = 6.dp, + strokeCap = StrokeCap.Round, + ) + CircularProgressIndicator( + progress = { progress.coerceAtMost(1f) }, + modifier = Modifier.fillMaxSize(), + color = color, + strokeWidth = 6.dp, + strokeCap = StrokeCap.Round, + ) + Text( + text = "${(progress * 100).toInt()}%", + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Bold + ) + } + Spacer(Modifier.height(4.dp)) + Text(text = label, style = MaterialTheme.typography.labelMedium) + Text(text = "$current / $target", style = MaterialTheme.typography.labelSmall, color = Color.Gray) + } +} + @Composable fun HistoryScreen(dao: AppDao, prefs: SharedPreferences) { var selectedDate by remember { mutableStateOf(Calendar.getInstance()) } @@ -787,12 +870,16 @@ fun HistoryScreen(dao: AppDao, prefs: SharedPreferences) { var selectedMealForDetail by remember { mutableStateOf(null) } val context = LocalContext.current + val tCal = prefs.getString("target_calories", "2000")?.toIntOrNull() ?: 2000 + val tCarb = prefs.getString("target_carbs", "250")?.toIntOrNull() ?: 250 + val tProt = prefs.getString("target_protein", "100")?.toIntOrNull() ?: 100 + val tFat = prefs.getString("target_fat", "60")?.toIntOrNull() ?: 60 + if (selectedMealForDetail != null) { AlertDialog( onDismissRequest = { selectedMealForDetail = null }, title = { Text(selectedMealForDetail!!.name) }, text = { - // Ensure history detail dialog scrolls Column(modifier = Modifier .fillMaxWidth() .heightIn(max = 450.dp) @@ -800,6 +887,7 @@ fun HistoryScreen(dao: AppDao, prefs: SharedPreferences) { ) { Text("Type: ${selectedMealForDetail!!.type}", fontWeight = FontWeight.Bold) Text("Calories: ${selectedMealForDetail!!.totalCalories} kcal") + Text("Macro: G ${selectedMealForDetail!!.carbs}g | P ${selectedMealForDetail!!.protein}g | L ${selectedMealForDetail!!.fat}g") Text("Heure: ${SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date(selectedMealForDetail!!.date))}") Spacer(Modifier.height(8.dp)) Text("Analyse complète :", style = MaterialTheme.typography.labelMedium) @@ -843,23 +931,27 @@ fun HistoryScreen(dao: AppDao, prefs: SharedPreferences) { val totalIn = meals.sumOf { it.totalCalories } val totalOut = sports.sumOf { it.calories?.toInt() ?: 0 } - val balance = totalIn - totalOut + val totalCarbs = meals.sumOf { it.carbs } + val totalProt = meals.sumOf { it.protein } + val totalFat = meals.sumOf { it.fat } Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) { Column(modifier = Modifier.padding(16.dp)) { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Text("Calories Consommées:") - Text("$totalIn kcal", fontWeight = FontWeight.Bold) + Text("Objectifs Quotidiens", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + Spacer(Modifier.height(12.dp)) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { + DailyGoalChart("Calories", totalIn, tCal, Color(0xFF4CAF50)) + DailyGoalChart("Glucides", totalCarbs, tCarb, Color(0xFF2196F3)) + DailyGoalChart("Protéines", totalProt, tProt, Color(0xFFFF9800)) + DailyGoalChart("Lipides", totalFat, tFat, Color(0xFFE91E63)) } + Spacer(Modifier.height(12.dp)) + HorizontalDivider() + Spacer(Modifier.height(8.dp)) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Text("Calories Brûlées (Sport):") + Text("Sport (Brûlées):") Text("$totalOut kcal", fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) } - HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Text("Bilan net:", fontWeight = FontWeight.Bold) - Text("$balance kcal", fontWeight = FontWeight.ExtraBold, color = if(balance > 0) Color.Red else Color.Green) - } } } @@ -877,7 +969,6 @@ fun HistoryScreen(dao: AppDao, prefs: SharedPreferences) { Text(category, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.secondary, modifier = Modifier.padding(top = 12.dp, bottom = 4.dp)) } - // Display Before Glycemia if (isDiabetic && beforeGly != null) { item { Surface(color = Color.Blue.copy(alpha = 0.1f), modifier = Modifier.fillMaxWidth().clip(MaterialTheme.shapes.small)) { @@ -889,7 +980,7 @@ fun HistoryScreen(dao: AppDao, prefs: SharedPreferences) { items(categoryMeals) { meal -> ListItem( headlineContent = { Text(meal.name) }, - supportingContent = { Text("${meal.totalCalories} kcal - ${SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date(meal.date))}") }, + supportingContent = { Text("${meal.totalCalories} kcal - G:${meal.carbs}g P:${meal.protein}g L:${meal.fat}g") }, trailingContent = { IconButton(onClick = { /* TODO */ }) { Icon(Icons.Default.Delete, null) } }, @@ -897,7 +988,6 @@ fun HistoryScreen(dao: AppDao, prefs: SharedPreferences) { ) } - // Display After Glycemia if (isDiabetic && afterGly != null) { item { Surface(color = Color.Green.copy(alpha = 0.1f), modifier = Modifier.fillMaxWidth().clip(MaterialTheme.shapes.small)) { @@ -928,7 +1018,7 @@ private fun analyzeImage( bitmap: Bitmap?, textDescription: String?, setAnalyzing: (Boolean) -> Unit, - onResult: (Triple?, String?) -> Unit, + onResult: (Triple>?, String?) -> Unit, scope: CoroutineScope ) { setAnalyzing(true) @@ -940,10 +1030,12 @@ private fun analyzeImage( base64 = Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP) } - val prompt = if (bitmap != null) { - "Analyze this food image. Provide ONLY: 1. Name (ex: 'Bol de Ramen'), 2. Summary description, 3. Total calories. Format EXACTLY as: {\"name\": \"...\", \"description\": \"...\", \"calories\": integer}" + val prompt = if (bitmap != null && textDescription == null) { + "Analyze this food image in FRENCH. Provide ONLY: 1. Name, 2. Summary description in FRENCH, 3. Macros. Format EXACTLY as: {\"name\": \"...\", \"description\": \"...\", \"calories\": int, \"carbs\": int, \"protein\": int, \"fat\": int}" + } else if (bitmap != null && textDescription != null) { + "Analyze this food image in FRENCH, taking into account these corrections or details: '$textDescription'. Provide ONLY: 1. Name, 2. Summary description in FRENCH, 3. Macros. Format EXACTLY as: {\"name\": \"...\", \"description\": \"...\", \"calories\": int, \"carbs\": int, \"protein\": int, \"fat\": int}" } else { - "Analyze this meal description: '$textDescription'. Estimate the calories. Provide ONLY a JSON object. Format EXACTLY as: {\"name\": \"...\", \"description\": \"...\", \"calories\": integer}" + "Analyze this meal description in FRENCH: '$textDescription'. Estimate the macros. Provide ONLY a JSON object. Format EXACTLY as: {\"name\": \"...\", \"description\": \"...\", \"calories\": int, \"carbs\": int, \"protein\": int, \"fat\": int}" } scope.launch { @@ -958,7 +1050,6 @@ private fun analyzeImage( ) val responseStr = response.string() - // Extract JSON val jsonStartIndex = responseStr.indexOf("{") val jsonEndIndex = responseStr.lastIndexOf("}") + 1 @@ -969,20 +1060,17 @@ private fun analyzeImage( onResult(Triple( json.optString("name", textDescription ?: "Repas"), json.optString("description", "Analyse réussie"), - json.optInt("calories", 0) + listOf( + json.optInt("calories", 0), + json.optInt("carbs", 0), + json.optInt("protein", 0), + json.optInt("fat", 0) + ) ), null) return@launch - } catch (je: Exception) { } + } catch (_: Exception) { } } - - // Fallback for text - val nameMatch = "(?:Nom|Name)\\s*[:\\-]?\\s*([^\\n.*]+)".toRegex(RegexOption.IGNORE_CASE).find(responseStr) - val calMatch = "(?:Total|Calories|Kcal)\\s*[:\\-]?\\s*(\\d+)".toRegex(RegexOption.IGNORE_CASE).find(responseStr) - - val detectedName = nameMatch?.groupValues?.get(1)?.trim() ?: textDescription ?: "Repas" - val calories = calMatch?.groupValues?.get(1)?.toIntOrNull() ?: 0 - onResult(Triple(detectedName, responseStr.take(1000), calories), null) - + onResult(null, "Format de réponse inconnu") } catch (e: Exception) { onResult(null, e.localizedMessage ?: "Erreur réseau") } finally { @@ -1028,7 +1116,6 @@ private fun syncStravaActivities(dao: AppDao, prefs: SharedPreferences, scope: C @Composable fun SportScreen(dao: AppDao, prefs: SharedPreferences) { val sports by dao.getAllSports().collectAsState(initial = emptyList()) - val weightKg = prefs.getString("weight_kg", "70")?.toDoubleOrNull() ?: 70.0 val coroutineScope = rememberCoroutineScope() val context = LocalContext.current @@ -1063,7 +1150,6 @@ fun SportScreen(dao: AppDao, prefs: SharedPreferences) { } } -// Helper to format float fun Float.format(digits: Int) = "%.${digits}f".format(this) @Composable diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..822b621 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..cf7243a Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..b272280 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..0f91292 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..a5d268b Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ