test
This commit is contained in:
3
.idea/misc.xml
generated
3
.idea/misc.xml
generated
@@ -1,6 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ fun AuthWrapper(dao: AppDao) {
|
|||||||
|
|
||||||
var isAuthorized by remember { mutableStateOf<Boolean?>(null) }
|
var isAuthorized by remember { mutableStateOf<Boolean?>(null) }
|
||||||
|
|
||||||
|
// État pour savoir si la synchro initiale est terminée
|
||||||
|
var syncCompleted by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
|
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
|
||||||
try {
|
try {
|
||||||
@@ -77,8 +80,8 @@ fun AuthWrapper(dao: AppDao) {
|
|||||||
} catch (e: ApiException) {
|
} catch (e: ApiException) {
|
||||||
Log.e("Auth", "Erreur Google Sign-In : ${e.statusCode}")
|
Log.e("Auth", "Erreur Google Sign-In : ${e.statusCode}")
|
||||||
val msg = when (e.statusCode) {
|
val msg = when (e.statusCode) {
|
||||||
10 -> "Erreur 10 : SHA-1 non reconnu. Assurez-vous d'avoir ajouté le SHA-1 de VOS clés."
|
10 -> "Erreur 10 : SHA-1 non reconnu."
|
||||||
7 -> "Erreur 7 : Problème de réseau."
|
7 -> "Erreur 7 : Réseau."
|
||||||
else -> "Erreur Google (Code ${e.statusCode})."
|
else -> "Erreur Google (Code ${e.statusCode})."
|
||||||
}
|
}
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_LONG).show()
|
Toast.makeText(context, msg, Toast.LENGTH_LONG).show()
|
||||||
@@ -90,6 +93,7 @@ fun AuthWrapper(dao: AppDao) {
|
|||||||
googleSignInClient.signOut().addOnCompleteListener {
|
googleSignInClient.signOut().addOnCompleteListener {
|
||||||
firebaseUser = null
|
firebaseUser = null
|
||||||
isAuthorized = null
|
isAuthorized = null
|
||||||
|
syncCompleted = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,23 +102,20 @@ fun AuthWrapper(dao: AppDao) {
|
|||||||
val email = firebaseUser?.email?.trim()?.lowercase()
|
val email = firebaseUser?.email?.trim()?.lowercase()
|
||||||
|
|
||||||
if (email != null && email.isNotEmpty()) {
|
if (email != null && email.isNotEmpty()) {
|
||||||
Log.d("Auth", "Vérification de l'autorisation pour l'email: '$email'")
|
|
||||||
try {
|
try {
|
||||||
val db = FirebaseFirestore.getInstance("scan-wich")
|
val db = FirebaseFirestore.getInstance("scan-wich")
|
||||||
val docRef = db.collection("authorized_users").document(email)
|
val docRef = db.collection("authorized_users").document(email)
|
||||||
val document = docRef.get().await()
|
val document = docRef.get().await()
|
||||||
|
|
||||||
if (document.exists()) {
|
if (document.exists()) {
|
||||||
Log.d("Auth", "Accès AUTORISÉ pour '$email'.")
|
Log.d("Auth", "Accès autorisé. Lancement de la synchronisation...")
|
||||||
|
|
||||||
// --- NOUVEAU : Rapatriement des données ---
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
|
||||||
FirebaseUtils.fetchAllDataFromFirestore(dao)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
val prefs = ApiClient.getEncryptedPrefs(context)
|
||||||
|
// On attend que la synchro soit finie pour afficher l'app
|
||||||
|
FirebaseUtils.fetchAllDataFromFirestore(dao, prefs)
|
||||||
|
syncCompleted = true
|
||||||
isAuthorized = true
|
isAuthorized = true
|
||||||
} else {
|
} else {
|
||||||
Log.w("Auth", "Accès REFUSÉ pour '$email'.")
|
|
||||||
isAuthorized = false
|
isAuthorized = false
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -130,15 +131,26 @@ fun AuthWrapper(dao: AppDao) {
|
|||||||
LoginScreen { launcher.launch(googleSignInClient.signInIntent) }
|
LoginScreen { launcher.launch(googleSignInClient.signInIntent) }
|
||||||
} else {
|
} else {
|
||||||
when (isAuthorized) {
|
when (isAuthorized) {
|
||||||
true -> MainApp(dao = dao, onLogout = onLogout, userId = firebaseUser!!.uid)
|
true -> {
|
||||||
false -> AccessDeniedScreen(onLogout)
|
if (syncCompleted) {
|
||||||
null -> {
|
MainApp(dao = dao, onLogout = onLogout, userId = firebaseUser!!.uid)
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
} else {
|
||||||
CircularProgressIndicator()
|
LoadingBox("Synchronisation de votre profil...")
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Text("Vérification et synchronisation...")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
false -> AccessDeniedScreen(onLogout)
|
||||||
|
null -> LoadingBox("Vérification de l'accès...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoadingBox(text: String) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,50 +8,49 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
|
|
||||||
@Entity(tableName = "meals")
|
@Entity(tableName = "meals")
|
||||||
data class Meal(
|
data class Meal(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
@PrimaryKey val date: Long = 0, // La date devient la clé unique pour éviter les doublons
|
||||||
val date: Long,
|
|
||||||
val name: String = "Repas",
|
val name: String = "Repas",
|
||||||
val analysisText: String,
|
val analysisText: String = "",
|
||||||
val totalCalories: Int,
|
val totalCalories: Int = 0,
|
||||||
val carbs: Int = 0,
|
val carbs: Int = 0,
|
||||||
val protein: Int = 0,
|
val protein: Int = 0,
|
||||||
val fat: Int = 0,
|
val fat: Int = 0,
|
||||||
val type: String = "Collation"
|
val type: String = "Collation",
|
||||||
|
val id: Int = 0 // On garde le champ id pour la compatibilité mais il n'est plus PrimaryKey
|
||||||
)
|
)
|
||||||
|
|
||||||
@Entity(tableName = "glycemia")
|
@Entity(tableName = "glycemia")
|
||||||
data class Glycemia(
|
data class Glycemia(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
@PrimaryKey val date: Long = 0, // La date devient la clé unique
|
||||||
val date: Long,
|
val value: Double = 0.0,
|
||||||
val value: Double,
|
val moment: String = "",
|
||||||
val moment: String
|
val id: Int = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
@Entity(tableName = "sports")
|
@Entity(tableName = "sports")
|
||||||
data class SportActivity(
|
data class SportActivity(
|
||||||
@PrimaryKey val id: Long,
|
@PrimaryKey val id: Long = 0, // Strava fournit déjà un ID unique
|
||||||
val name: String,
|
val name: String = "",
|
||||||
val type: String,
|
val type: String = "",
|
||||||
val distance: Float,
|
val distance: Float = 0f,
|
||||||
val movingTime: Int,
|
val movingTime: Int = 0,
|
||||||
val calories: Float?,
|
val calories: Float? = null,
|
||||||
val date: Long // timestamp
|
val date: Long = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
@Entity(tableName = "favorite_meals")
|
@Entity(tableName = "favorite_meals")
|
||||||
data class FavoriteMeal(
|
data class FavoriteMeal(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||||
val name: String,
|
val name: String = "",
|
||||||
val analysisText: String,
|
val analysisText: String = "",
|
||||||
val calories: Int,
|
val calories: Int = 0,
|
||||||
val carbs: Int,
|
val carbs: Int = 0,
|
||||||
val protein: Int,
|
val protein: Int = 0,
|
||||||
val fat: Int
|
val fat: Int = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface AppDao {
|
interface AppDao {
|
||||||
// On ajoute OnConflictStrategy.REPLACE pour la synchronisation
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertMeal(meal: Meal): Long
|
suspend fun insertMeal(meal: Meal): Long
|
||||||
|
|
||||||
@@ -61,7 +60,6 @@ interface AppDao {
|
|||||||
@Query("SELECT * FROM meals WHERE date >= :start AND date <= :end ORDER BY date ASC")
|
@Query("SELECT * FROM meals WHERE date >= :start AND date <= :end ORDER BY date ASC")
|
||||||
suspend fun getMealsInRangeSync(start: Long, end: Long): List<Meal>
|
suspend fun getMealsInRangeSync(start: Long, end: Long): List<Meal>
|
||||||
|
|
||||||
// On ajoute OnConflictStrategy.REPLACE pour la synchronisation
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertGlycemia(glycemia: Glycemia): Long
|
suspend fun insertGlycemia(glycemia: Glycemia): Long
|
||||||
|
|
||||||
@@ -87,23 +85,16 @@ interface AppDao {
|
|||||||
fun getAllDatesWithData(): Flow<List<Long>>
|
fun getAllDatesWithData(): Flow<List<Long>>
|
||||||
}
|
}
|
||||||
|
|
||||||
@Database(entities = [Meal::class, Glycemia::class, SportActivity::class, FavoriteMeal::class], version = 7)
|
@Database(entities = [Meal::class, Glycemia::class, SportActivity::class, FavoriteMeal::class], version = 8) // Version incrémentée
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun appDao(): AppDao
|
abstract fun appDao(): AppDao
|
||||||
companion object {
|
companion object {
|
||||||
@Volatile private var INSTANCE: AppDatabase? = null
|
@Volatile private var INSTANCE: AppDatabase? = null
|
||||||
|
|
||||||
private val MIGRATION_6_7 = object : Migration(6, 7) {
|
|
||||||
override fun migrate(db: SupportSQLiteDatabase) {
|
|
||||||
db.execSQL("CREATE TABLE IF NOT EXISTS `favorite_meals` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `analysisText` TEXT NOT NULL, `calories` INTEGER NOT NULL, `carbs` INTEGER NOT NULL, `protein` INTEGER NOT NULL, `fat` INTEGER NOT NULL)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDatabase(context: Context): AppDatabase =
|
fun getDatabase(context: Context): AppDatabase =
|
||||||
INSTANCE ?: synchronized(this) {
|
INSTANCE ?: synchronized(this) {
|
||||||
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_db")
|
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_db")
|
||||||
.addMigrations(MIGRATION_6_7)
|
.fallbackToDestructiveMigration() // Ceci va vider la base locale une seule fois pour appliquer le changement
|
||||||
.fallbackToDestructiveMigration(dropAllTables = false)
|
|
||||||
.build().also { INSTANCE = it }
|
.build().also { INSTANCE = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.example.scanwich
|
package com.example.scanwich
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.content.edit
|
||||||
import com.google.firebase.auth.FirebaseAuth
|
import com.google.firebase.auth.FirebaseAuth
|
||||||
import com.google.firebase.firestore.FirebaseFirestore
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
import com.google.firebase.firestore.SetOptions
|
import com.google.firebase.firestore.SetOptions
|
||||||
@@ -48,33 +50,60 @@ object FirebaseUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOUVELLE FONCTION : Rapatrie toutes les données depuis le Cloud
|
suspend fun fetchAllDataFromFirestore(dao: AppDao, prefs: SharedPreferences) {
|
||||||
suspend fun fetchAllDataFromFirestore(dao: AppDao) {
|
|
||||||
val user = FirebaseAuth.getInstance().currentUser ?: return
|
val user = FirebaseAuth.getInstance().currentUser ?: return
|
||||||
val db = getDb()
|
val db = getDb()
|
||||||
val userDoc = db.collection("users").document(user.uid)
|
val userDoc = db.collection("users").document(user.uid)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Récupérer les repas
|
Log.d("FirestoreSync", "Début de la récupération pour l'UID: ${user.uid}")
|
||||||
|
|
||||||
|
// 1. Profil
|
||||||
|
val profileSnapshot = userDoc.get().await()
|
||||||
|
if (profileSnapshot.exists()) {
|
||||||
|
prefs.edit {
|
||||||
|
profileSnapshot.get("target_calories")?.let { putString("target_calories", it.toString()) }
|
||||||
|
profileSnapshot.get("target_carbs")?.let { putString("target_carbs", it.toString()) }
|
||||||
|
profileSnapshot.get("target_protein")?.let { putString("target_protein", it.toString()) }
|
||||||
|
profileSnapshot.get("target_fat")?.let { putString("target_fat", it.toString()) }
|
||||||
|
profileSnapshot.get("weight_kg")?.let { putString("weight_kg", it.toString()) }
|
||||||
|
profileSnapshot.getBoolean("is_lbs")?.let { putBoolean("is_lbs", it) }
|
||||||
|
profileSnapshot.get("height_cm")?.let { putString("height_cm", it.toString()) }
|
||||||
|
profileSnapshot.getBoolean("is_diabetic")?.let { putBoolean("is_diabetic", it) }
|
||||||
|
(profileSnapshot.get("age") as? Number)?.let { putInt("age", it.toInt()) }
|
||||||
|
profileSnapshot.getString("gender")?.let { putString("gender", it) }
|
||||||
|
profileSnapshot.getString("activity_level")?.let { putString("activity_level", it) }
|
||||||
|
profileSnapshot.getString("goal")?.let { putString("goal", it) }
|
||||||
|
}
|
||||||
|
Log.d("FirestoreSync", "Profil récupéré avec succès")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Repas
|
||||||
val mealsSnapshot = userDoc.collection("meals").get().await()
|
val mealsSnapshot = userDoc.collection("meals").get().await()
|
||||||
val meals = mealsSnapshot.toObjects(Meal::class.java)
|
if (!mealsSnapshot.isEmpty) {
|
||||||
meals.forEach { dao.insertMeal(it) }
|
val meals = mealsSnapshot.toObjects(Meal::class.java)
|
||||||
Log.d("FirestoreSync", "${meals.size} repas récupérés")
|
meals.forEach { dao.insertMeal(it) }
|
||||||
|
Log.d("FirestoreSync", "${meals.size} repas insérés dans la base locale")
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Récupérer la glycémie
|
// 3. Glycémie
|
||||||
val glycemiaSnapshot = userDoc.collection("glycemia").get().await()
|
val glycemiaSnapshot = userDoc.collection("glycemia").get().await()
|
||||||
val glycemia = glycemiaSnapshot.toObjects(Glycemia::class.java)
|
if (!glycemiaSnapshot.isEmpty) {
|
||||||
glycemia.forEach { dao.insertGlycemia(it) }
|
val glycemia = glycemiaSnapshot.toObjects(Glycemia::class.java)
|
||||||
Log.d("FirestoreSync", "${glycemia.size} glycémies récupérées")
|
glycemia.forEach { dao.insertGlycemia(it) }
|
||||||
|
Log.d("FirestoreSync", "${glycemia.size} relevés de glycémie insérés")
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Récupérer le sport
|
// 4. Sport
|
||||||
val sportsSnapshot = userDoc.collection("sports").get().await()
|
val sportsSnapshot = userDoc.collection("sports").get().await()
|
||||||
val sports = sportsSnapshot.toObjects(SportActivity::class.java)
|
if (!sportsSnapshot.isEmpty) {
|
||||||
dao.insertSports(sports)
|
val sports = sportsSnapshot.toObjects(SportActivity::class.java)
|
||||||
Log.d("FirestoreSync", "${sports.size} activités sportives récupérées")
|
dao.insertSports(sports)
|
||||||
|
Log.d("FirestoreSync", "${sports.size} activités sportives insérées")
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("FirestoreSync", "Erreur lors du rapatriement des données: ${e.message}")
|
Log.e("FirestoreSync", "ERREUR CRITIQUE lors du rapatriement: ${e.message}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlinx.coroutines.tasks.await
|
import kotlinx.coroutines.tasks.await
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
// --- OPEN FOOD FACTS API ---
|
// --- OPEN FOOD FACTS API ---
|
||||||
data class OffProductResponse(val status: Int, val product: OffProduct?)
|
data class OffProductResponse(val status: Int, val product: OffProduct?)
|
||||||
@@ -71,7 +72,6 @@ object ApiClient {
|
|||||||
.create(StravaApi::class.java)
|
.create(StravaApi::class.java)
|
||||||
|
|
||||||
val offApi: OffApi = Retrofit.Builder()
|
val offApi: OffApi = Retrofit.Builder()
|
||||||
// On force l'utilisation de l'API française pour les codes-barres
|
|
||||||
.baseUrl("https://fr.openfoodfacts.org/api/v2/")
|
.baseUrl("https://fr.openfoodfacts.org/api/v2/")
|
||||||
.client(okHttpClient)
|
.client(okHttpClient)
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
@@ -79,12 +79,31 @@ object ApiClient {
|
|||||||
.create(OffApi::class.java)
|
.create(OffApi::class.java)
|
||||||
|
|
||||||
private const val STRAVA_CLIENT_ID = "203805"
|
private const val STRAVA_CLIENT_ID = "203805"
|
||||||
|
private const val PREFS_FILENAME = "secure_user_prefs"
|
||||||
|
|
||||||
fun getEncryptedPrefs(context: Context): SharedPreferences {
|
fun getEncryptedPrefs(context: Context): SharedPreferences {
|
||||||
|
return try {
|
||||||
|
createPrefs(context)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Security", "Erreur Keystore: ${e.message}. Réinitialisation...")
|
||||||
|
try {
|
||||||
|
// Version plus sûre pour effacer les prefs corrompues
|
||||||
|
val prefsFile = File(context.filesDir.parent, "shared_prefs/$PREFS_FILENAME.xml")
|
||||||
|
if (prefsFile.exists()) {
|
||||||
|
prefsFile.delete()
|
||||||
|
}
|
||||||
|
createPrefs(context)
|
||||||
|
} catch (e2: Exception) {
|
||||||
|
context.getSharedPreferences(PREFS_FILENAME, Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPrefs(context: Context): SharedPreferences {
|
||||||
val masterKey = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
|
val masterKey = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
|
||||||
return EncryptedSharedPreferences.create(
|
return EncryptedSharedPreferences.create(
|
||||||
context,
|
context,
|
||||||
"secure_user_prefs",
|
PREFS_FILENAME,
|
||||||
masterKey,
|
masterKey,
|
||||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||||
|
|||||||
@@ -2,16 +2,17 @@
|
|||||||
|
|
||||||
**Nouveautés de la version actuelle :**
|
**Nouveautés de la version actuelle :**
|
||||||
|
|
||||||
|
☁️ **Synchronisation Cloud Totale (Zéro Re-saisie) :**
|
||||||
|
- **Mémoire Cloud :** Votre profil (poids, objectifs, calories cibles) est désormais entièrement sauvegardé dans le Cloud. Plus besoin de retaper vos informations lors d'une mise à jour ou d'une réinstallation !
|
||||||
|
- **Rapatriement Automatique :** L'application récupère instantanément tout votre historique (repas, sport, glycémie) et vos paramètres dès la connexion.
|
||||||
|
- **Modèles de données optimisés :** Mise à jour des structures internes pour garantir une compatibilité parfaite avec Firebase lors de la récupération des données.
|
||||||
|
- **Résilience Keystore :** Ajout d'un système d'auto-réparation en cas de corruption des clés de sécurité locales, évitant ainsi les fermetures inattendues de l'application.
|
||||||
|
|
||||||
🍲 **Focus Nourriture Pur (IA) :**
|
🍲 **Focus Nourriture Pur (IA) :**
|
||||||
- **Analyse sélective :** L'intelligence artificielle se concentre désormais exclusivement sur la nourriture. Les qualificatifs, les descriptions de l'environnement ou les éléments de décor sont ignorés pour ne garder que l'essentiel nutritionnel.
|
- **Analyse sélective :** L'intelligence artificielle se concentre désormais exclusivement sur la nourriture. Les qualificatifs, les descriptions de l'environnement ou les éléments de décor sont ignorés pour ne garder que l'essentiel nutritionnel.
|
||||||
|
|
||||||
🔔 **Rappels de Repas :**
|
🔔 **Rappels de Repas :**
|
||||||
- **Notifications intelligentes :** Ne manquez plus un enregistrement ! Des rappels automatiques ont été ajoutés pour le déjeuner (08h30), le dîner (12h30) et le souper (19h30).
|
- **Notifications intelligentes :** Ne manquez plus un enregistrement ! Des rappels automatiques ont été ajoutés pour le déjeuner (08h30), le dîner (12h30) et le souper (19h30).
|
||||||
- **Relance automatique :** Les notifications se réactivent automatiquement même après un redémarrage du téléphone.
|
|
||||||
|
|
||||||
📊 **Résumé Calorique Avancé :**
|
|
||||||
- **Bilan Net :** L'historique affiche maintenant le calcul "Mangé - Dépensé (Sport) = Total Net" pour une vision précise de votre équilibre journalier.
|
|
||||||
- **Alertes visuelles :** Le total net passe en rouge si vous dépassez vos objectifs quotidiens.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -27,11 +28,9 @@
|
|||||||
🚀 **Connexion Strava 100% Automatique :**
|
🚀 **Connexion Strava 100% Automatique :**
|
||||||
- **Simplicité totale :** Connexion en un seul clic sans saisie d'identifiants techniques.
|
- **Simplicité totale :** Connexion en un seul clic sans saisie d'identifiants techniques.
|
||||||
|
|
||||||
🎨 **Améliorations UI/UX :**
|
🛡️ **Architecture Cloud & Sécurité :**
|
||||||
- **Correction du Thème :** Lisibilité parfaite sur l'écran de profil.
|
- **Gestion des accès :** Contrôle dynamique des utilisateurs autorisés via Firestore.
|
||||||
|
- **Secret Manager :** Protection maximale des clés API via Google Cloud.
|
||||||
🛡️ **Architecture Cloud :**
|
|
||||||
- **Profils Cloud :** Synchronisation automatique de vos données personnelles.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user