test
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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,53 +616,88 @@ fun CaptureScreen(dao: AppDao, prefs: SharedPreferences, isDiabetic: Boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
if (showMealDialog && currentMealData != null) {
|
||||
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) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { showMealDialog = false },
|
||||
title = { Text("Détails du repas") },
|
||||
text = {
|
||||
// Ensure the content inside the dialog scrolls
|
||||
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
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 450.dp)
|
||||
.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()
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = editableCalories,
|
||||
onValueChange = { editableCalories = it },
|
||||
label = { Text("Calories (kcal)") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
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") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
label = { Text("Description / Précisions pour l'IA") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = 3
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text("Type de repas :")
|
||||
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
|
||||
) {
|
||||
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)
|
||||
FilterChip(selected = mealType == type, onClick = { mealType = type }, label = { Text(type) })
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Button(onClick = {
|
||||
DatePickerDialog(context, { _, y, m, d ->
|
||||
calendar.set(y, m, d)
|
||||
@@ -667,28 +707,40 @@ fun CaptureScreen(dao: AppDao, prefs: SharedPreferences, isDiabetic: Boolean) {
|
||||
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)))
|
||||
}, 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)))
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
|
||||
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
|
||||
))
|
||||
showMealDialog = false
|
||||
showBottomSheet = false
|
||||
capturedBitmap = null
|
||||
Toast.makeText(context, "Repas enregistré !", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}) { Text("Enregistrer") }
|
||||
},
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user