changes #2

Merged
macharest merged 2 commits from changes into master 2026-02-22 18:24:42 -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 name: String = "Repas",
val analysisText: String, val analysisText: String,
val totalCalories: Int, val totalCalories: Int,
val carbs: Int = 0,
val protein: Int = 0,
val fat: Int = 0,
val type: String = "Collation" val type: String = "Collation"
) )
@@ -53,7 +56,7 @@ interface AppDao {
fun getSportsForDay(startOfDay: Long, endOfDay: Long): Flow<List<SportActivity>> 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 class AppDatabase : RoomDatabase() {
abstract fun appDao(): AppDao abstract fun appDao(): AppDao
companion object { companion object {

View File

@@ -37,6 +37,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight 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.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable 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.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.common.api.ApiException import com.google.android.gms.common.api.ApiException
import com.google.gson.annotations.SerializedName
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@@ -75,7 +76,7 @@ data class N8nMealRequest(
) )
interface N8nApi { interface N8nApi {
@POST("webhook/v1/gemini-proxy") @POST("webhook-test/v1/gemini-proxy")
suspend fun analyzeMeal( suspend fun analyzeMeal(
@Header("X-API-KEY") apiKey: String, @Header("X-API-KEY") apiKey: String,
@Body request: N8nMealRequest @Body request: N8nMealRequest
@@ -128,9 +129,6 @@ interface StravaApi {
): StravaTokenResponse ): StravaTokenResponse
} }
// Annotation for Gson
annotation class SerializedName(val value: String)
// Helpers // Helpers
object ApiClient { object ApiClient {
private val loggingInterceptor = HttpLoggingInterceptor().apply { private val loggingInterceptor = HttpLoggingInterceptor().apply {
@@ -514,11 +512,16 @@ fun SetupScreen(prefs: SharedPreferences, onComplete: () -> Unit) {
var targetCals = (bmr * multiplier).toInt() var targetCals = (bmr * multiplier).toInt()
if (goal == "Perdre du poids") targetCals -= 500 if (goal == "Perdre du poids") targetCals -= 500
val targetCarbs = (targetCals * 0.5 / 4).toInt() val targetCarbs = (targetCals * 0.5 / 4).toInt()
val targetProtein = (targetCals * 0.2 / 4).toInt()
val targetFat = (targetCals * 0.3 / 9).toInt()
prefs.edit { prefs.edit {
putString("target_calories", targetCals.toString()) putString("target_calories", targetCals.toString())
putString("target_carbs", targetCarbs.toString()) putString("target_carbs", targetCarbs.toString())
putString("target_protein", targetProtein.toString())
putString("target_fat", targetFat.toString())
putString("weight_kg", weightKg.toString()) putString("weight_kg", weightKg.toString())
putString("weight_display", weightDisplay) putString("weight_display", weightDisplay)
putBoolean("is_lbs", isLbs) putBoolean("is_lbs", isLbs)
@@ -539,6 +542,7 @@ fun SetupScreen(prefs: SharedPreferences, onComplete: () -> Unit) {
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun CaptureScreen(dao: AppDao, prefs: SharedPreferences, isDiabetic: Boolean) { fun CaptureScreen(dao: AppDao, prefs: SharedPreferences, isDiabetic: Boolean) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -546,9 +550,10 @@ fun CaptureScreen(dao: AppDao, prefs: SharedPreferences, isDiabetic: Boolean) {
var capturedBitmap by remember { mutableStateOf<Bitmap?>(null) } var capturedBitmap by remember { mutableStateOf<Bitmap?>(null) }
var isAnalyzing by remember { mutableStateOf(false) } var isAnalyzing by remember { mutableStateOf(false) }
var showMealDialog by remember { mutableStateOf(false) } var currentMealData by remember { mutableStateOf<Triple<String, String, List<Int>>?>(null) }
var currentMealData by remember { mutableStateOf<Triple<String, String, Int>?>(null) }
var mealDateTime by remember { mutableLongStateOf(System.currentTimeMillis()) } var mealDateTime by remember { mutableLongStateOf(System.currentTimeMillis()) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)
var showBottomSheet by remember { mutableStateOf(false) }
var manualMealName by remember { mutableStateOf("") } var manualMealName by remember { mutableStateOf("") }
@@ -559,7 +564,7 @@ fun CaptureScreen(dao: AppDao, prefs: SharedPreferences, isDiabetic: Boolean) {
analyzeImage(bitmap, null, { isAnalyzing = it }, { data, error -> analyzeImage(bitmap, null, { isAnalyzing = it }, { data, error ->
if (data != null) { if (data != null) {
currentMealData = data currentMealData = data
showMealDialog = true showBottomSheet = true
} else { } else {
Toast.makeText(context, "Analyse échouée : $error", Toast.LENGTH_LONG).show() 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 -> analyzeImage(bitmap, null, { isAnalyzing = it }, { data, error ->
if (data != null) { if (data != null) {
currentMealData = data currentMealData = data
showMealDialog = true showBottomSheet = true
} else { } else {
Toast.makeText(context, "L'IA n'a pas pu identifier le repas.", Toast.LENGTH_LONG).show() 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) { if (showBottomSheet && currentMealData != null) {
var mealType by remember { mutableStateOf("Déjeuner") } ModalBottomSheet(
val calendar = Calendar.getInstance().apply { timeInMillis = mealDateTime } onDismissRequest = { showBottomSheet = false },
sheetState = sheetState,
var editableName by remember { mutableStateOf(currentMealData!!.first) } containerColor = MaterialTheme.colorScheme.surface,
var editableCalories by remember { mutableStateOf(currentMealData!!.third.toString()) } modifier = Modifier.fillMaxHeight(0.85f)
var editableDesc by remember { mutableStateOf(currentMealData!!.second) } ) {
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( // Update local state if currentMealData changes (e.g. after resubmission)
onDismissRequest = { showMealDialog = false }, LaunchedEffect(currentMealData) {
title = { Text("Détails du repas") }, editableName = currentMealData!!.first
text = { editableDesc = currentMealData!!.second
// Ensure the content inside the dialog scrolls }
Column(modifier = Modifier
.fillMaxWidth() Column(modifier = Modifier
.heightIn(max = 450.dp) .padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()) .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( Icon(Icons.Default.Refresh, null)
value = editableName, Spacer(Modifier.width(8.dp))
onValueChange = { editableName = it }, Text("Ressoumettre à l'IA")
label = { Text("Nom du repas") }, }
modifier = Modifier.fillMaxWidth()
) Spacer(Modifier.height(16.dp))
OutlinedTextField( Text("Catégorie :", fontWeight = FontWeight.Bold)
value = editableCalories, Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
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 :")
listOf("Déjeuner", "Dîner", "Souper", "Collation").forEach { type -> listOf("Déjeuner", "Dîner", "Souper", "Collation").forEach { type ->
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { mealType = type }) { FilterChip(selected = mealType == type, onClick = { mealType = type }, label = { Text(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)))
} }
} }
},
confirmButton = { Spacer(Modifier.height(16.dp))
Button(onClick = { Button(onClick = {
coroutineScope.launch { DatePickerDialog(context, { _, y, m, d ->
dao.insertMeal(Meal( calendar.set(y, m, d)
date = mealDateTime, TimePickerDialog(context, { _, hh, mm ->
name = editableName, calendar.set(Calendar.HOUR_OF_DAY, hh)
analysisText = editableDesc, calendar.set(Calendar.MINUTE, mm)
totalCalories = editableCalories.toIntOrNull() ?: 0, mealDateTime = calendar.timeInMillis
type = mealType }, 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()
showMealDialog = false }, modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer)) {
capturedBitmap = null Icon(Icons.Default.DateRange, null)
Toast.makeText(context, "Repas enregistré !", Toast.LENGTH_SHORT).show() Spacer(Modifier.width(8.dp))
} Text("Date/Heure: " + SimpleDateFormat("dd/MM HH:mm", Locale.getDefault()).format(Date(mealDateTime)))
}) { Text("Enregistrer") } }
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())) { 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 -> analyzeImage(null, manualMealName, { isAnalyzing = it }, { data, error ->
if (data != null) { if (data != null) {
currentMealData = data currentMealData = data
showMealDialog = true showBottomSheet = true
} else { } else {
Toast.makeText(context, "Erreur AI: $error", Toast.LENGTH_LONG).show() Toast.makeText(context, "Erreur AI: $error", Toast.LENGTH_LONG).show()
} }
@@ -758,9 +810,9 @@ fun CaptureScreen(dao: AppDao, prefs: SharedPreferences, isDiabetic: Boolean) {
OutlinedButton( OutlinedButton(
onClick = { onClick = {
currentMealData = Triple(manualMealName, "Ajout manuel", 0) currentMealData = Triple(manualMealName, "Ajout manuel", listOf(0, 0, 0, 0))
mealDateTime = System.currentTimeMillis() mealDateTime = System.currentTimeMillis()
showMealDialog = true showBottomSheet = true
}, },
enabled = manualMealName.isNotBlank() && !isAnalyzing, enabled = manualMealName.isNotBlank() && !isAnalyzing,
modifier = Modifier.weight(1f) 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 @Composable
fun HistoryScreen(dao: AppDao, prefs: SharedPreferences) { fun HistoryScreen(dao: AppDao, prefs: SharedPreferences) {
var selectedDate by remember { mutableStateOf(Calendar.getInstance()) } var selectedDate by remember { mutableStateOf(Calendar.getInstance()) }
@@ -787,12 +870,16 @@ fun HistoryScreen(dao: AppDao, prefs: SharedPreferences) {
var selectedMealForDetail by remember { mutableStateOf<Meal?>(null) } var selectedMealForDetail by remember { mutableStateOf<Meal?>(null) }
val context = LocalContext.current 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) { if (selectedMealForDetail != null) {
AlertDialog( AlertDialog(
onDismissRequest = { selectedMealForDetail = null }, onDismissRequest = { selectedMealForDetail = null },
title = { Text(selectedMealForDetail!!.name) }, title = { Text(selectedMealForDetail!!.name) },
text = { text = {
// Ensure history detail dialog scrolls
Column(modifier = Modifier Column(modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(max = 450.dp) .heightIn(max = 450.dp)
@@ -800,6 +887,7 @@ fun HistoryScreen(dao: AppDao, prefs: SharedPreferences) {
) { ) {
Text("Type: ${selectedMealForDetail!!.type}", fontWeight = FontWeight.Bold) Text("Type: ${selectedMealForDetail!!.type}", fontWeight = FontWeight.Bold)
Text("Calories: ${selectedMealForDetail!!.totalCalories} kcal") 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))}") Text("Heure: ${SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date(selectedMealForDetail!!.date))}")
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Text("Analyse complète :", style = MaterialTheme.typography.labelMedium) 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 totalIn = meals.sumOf { it.totalCalories }
val totalOut = sports.sumOf { it.calories?.toInt() ?: 0 } 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)) { Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text("Objectifs Quotidiens", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text("Calories Consommées:") Spacer(Modifier.height(12.dp))
Text("$totalIn kcal", fontWeight = FontWeight.Bold) 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) { 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) 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)) 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) { if (isDiabetic && beforeGly != null) {
item { item {
Surface(color = Color.Blue.copy(alpha = 0.1f), modifier = Modifier.fillMaxWidth().clip(MaterialTheme.shapes.small)) { 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 -> items(categoryMeals) { meal ->
ListItem( ListItem(
headlineContent = { Text(meal.name) }, 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 = { trailingContent = {
IconButton(onClick = { /* TODO */ }) { Icon(Icons.Default.Delete, null) } 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) { if (isDiabetic && afterGly != null) {
item { item {
Surface(color = Color.Green.copy(alpha = 0.1f), modifier = Modifier.fillMaxWidth().clip(MaterialTheme.shapes.small)) { Surface(color = Color.Green.copy(alpha = 0.1f), modifier = Modifier.fillMaxWidth().clip(MaterialTheme.shapes.small)) {
@@ -928,7 +1018,7 @@ private fun analyzeImage(
bitmap: Bitmap?, bitmap: Bitmap?,
textDescription: String?, textDescription: String?,
setAnalyzing: (Boolean) -> Unit, setAnalyzing: (Boolean) -> Unit,
onResult: (Triple<String, String, Int>?, String?) -> Unit, onResult: (Triple<String, String, List<Int>>?, String?) -> Unit,
scope: CoroutineScope scope: CoroutineScope
) { ) {
setAnalyzing(true) setAnalyzing(true)
@@ -940,10 +1030,12 @@ private fun analyzeImage(
base64 = Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP) base64 = Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
} }
val prompt = if (bitmap != null) { val prompt = if (bitmap != null && textDescription == 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}" "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 { } 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 { scope.launch {
@@ -958,7 +1050,6 @@ private fun analyzeImage(
) )
val responseStr = response.string() val responseStr = response.string()
// Extract JSON
val jsonStartIndex = responseStr.indexOf("{") val jsonStartIndex = responseStr.indexOf("{")
val jsonEndIndex = responseStr.lastIndexOf("}") + 1 val jsonEndIndex = responseStr.lastIndexOf("}") + 1
@@ -969,20 +1060,17 @@ private fun analyzeImage(
onResult(Triple( onResult(Triple(
json.optString("name", textDescription ?: "Repas"), json.optString("name", textDescription ?: "Repas"),
json.optString("description", "Analyse réussie"), 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) ), null)
return@launch return@launch
} catch (je: Exception) { } } catch (_: Exception) { }
} }
onResult(null, "Format de réponse inconnu")
// 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)
} catch (e: Exception) { } catch (e: Exception) {
onResult(null, e.localizedMessage ?: "Erreur réseau") onResult(null, e.localizedMessage ?: "Erreur réseau")
} finally { } finally {
@@ -1028,7 +1116,6 @@ private fun syncStravaActivities(dao: AppDao, prefs: SharedPreferences, scope: C
@Composable @Composable
fun SportScreen(dao: AppDao, prefs: SharedPreferences) { fun SportScreen(dao: AppDao, prefs: SharedPreferences) {
val sports by dao.getAllSports().collectAsState(initial = emptyList()) val sports by dao.getAllSports().collectAsState(initial = emptyList())
val weightKg = prefs.getString("weight_kg", "70")?.toDoubleOrNull() ?: 70.0
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current 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) fun Float.format(digits: Int) = "%.${digits}f".format(this)
@Composable @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