Compare commits

..

3 Commits

Author SHA1 Message Date
4712dae04a Merge pull request 'changes' (#2) from changes into master
Reviewed-on: http://192.168.2.134:3010/macharest/calorie/pulls/2
2026-02-22 18:24:40 -05:00
mac
1140dcc1fc test 2026-02-22 18:23:51 -05:00
mac
76efbec42c test 2026-02-22 18:22:26 -05:00
9 changed files with 252 additions and 116 deletions

47
app/google-services.json Normal file
View File

@@ -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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

View File

@@ -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<List<SportActivity>>
}
@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 {

View File

@@ -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<Bitmap?>(null) }
var isAnalyzing by remember { mutableStateOf(false) }
var showMealDialog by remember { mutableStateOf(false) }
var currentMealData by remember { mutableStateOf<Triple<String, String, Int>?>(null) }
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 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 }
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 editableCalories by remember { mutableStateOf(currentMealData!!.third.toString()) }
var editableDesc by remember { mutableStateOf(currentMealData!!.second) }
var editableName by remember { mutableStateOf(currentMealData!!.first) }
var editableDesc by remember { mutableStateOf(currentMealData!!.second) }
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())
val editableCalories = currentMealData!!.third[0].toString()
val editableCarbs = currentMealData!!.third[1].toString()
val editableProtein = currentMealData!!.third[2].toString()
val editableFat = currentMealData!!.third[3].toString()
// 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()
)
Icon(Icons.Default.Refresh, null)
Spacer(Modifier.width(8.dp))
Text("Ressoumettre à l'IA")
}
Spacer(Modifier.height(12.dp))
Text("Type de repas :")
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<Meal?>(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, String, Int>?, String?) -> Unit,
onResult: (Triple<String, String, List<Int>>?, 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB