Compare commits
2 Commits
7699ebcf2e
...
c9d05be4b1
| Author | SHA1 | Date | |
|---|---|---|---|
| c9d05be4b1 | |||
|
|
73a7f46509 |
@@ -18,7 +18,6 @@ android {
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
|
||||
// Incrémentation automatique du versionCode basé sur le temps
|
||||
versionCode = (System.currentTimeMillis() / 60000).toInt()
|
||||
versionName = "1.0." + (System.currentTimeMillis() / 3600000).toString().takeLast(3)
|
||||
|
||||
@@ -26,7 +25,6 @@ android {
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
// Chargement des propriétés depuis local.properties
|
||||
val keystoreProperties = Properties()
|
||||
val keystorePropertiesFile = rootProject.file("local.properties")
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
@@ -35,9 +33,9 @@ android {
|
||||
|
||||
getByName("debug") {
|
||||
storeFile = file(System.getProperty("user.home") + "/.android/debug.keystore")
|
||||
getByName("debug").storePassword = "android"
|
||||
getByName("debug").keyAlias = "androiddebugkey"
|
||||
getByName("debug").keyPassword = "android"
|
||||
storePassword = "android"
|
||||
keyAlias = "androiddebugkey"
|
||||
keyPassword = "android"
|
||||
}
|
||||
|
||||
create("release") {
|
||||
@@ -53,7 +51,7 @@ android {
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true // Activer l'offuscation
|
||||
isMinifyEnabled = true
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
@@ -111,9 +109,7 @@ dependencies {
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
|
||||
// SDK Firebase App Distribution COMPLET (API + Implémentation)
|
||||
implementation(libs.firebase.appdistribution)
|
||||
|
||||
implementation(libs.google.generativeai)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.androidx.exifinterface)
|
||||
@@ -133,17 +129,13 @@ dependencies {
|
||||
implementation(libs.firebase.firestore)
|
||||
implementation(libs.firebase.appcheck.playintegrity)
|
||||
|
||||
// Barcode Scanning & Camera
|
||||
implementation(libs.mlkit.barcode.scanning)
|
||||
implementation(libs.androidx.camera.core)
|
||||
implementation(libs.androidx.camera.camera2)
|
||||
implementation(libs.androidx.camera.lifecycle)
|
||||
implementation(libs.androidx.camera.view)
|
||||
|
||||
// PDF generation
|
||||
implementation(libs.itext7.core)
|
||||
|
||||
// Security
|
||||
implementation(libs.androidx.security.crypto)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
|
||||
@@ -100,24 +100,28 @@ fun AuthWrapper(dao: AppDao) {
|
||||
if (email != null && email.isNotEmpty()) {
|
||||
Log.d("Auth", "Vérification de l'autorisation pour l'email: '$email'")
|
||||
try {
|
||||
// On spécifie explicitement la base de données "scan-wich"
|
||||
val db = FirebaseFirestore.getInstance("scan-wich")
|
||||
val docRef = db.collection("authorized_users").document(email)
|
||||
val document = docRef.get().await()
|
||||
|
||||
if (document.exists()) {
|
||||
Log.d("Auth", "Accès AUTORISÉ pour '$email'. Document trouvé.")
|
||||
Log.d("Auth", "Accès AUTORISÉ pour '$email'.")
|
||||
|
||||
// --- NOUVEAU : Rapatriement des données ---
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
FirebaseUtils.fetchAllDataFromFirestore(dao)
|
||||
}
|
||||
|
||||
isAuthorized = true
|
||||
} else {
|
||||
Log.w("Auth", "Accès REFUSÉ pour '$email'. Document NON trouvé.")
|
||||
Log.w("Auth", "Accès REFUSÉ pour '$email'.")
|
||||
isAuthorized = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Auth", "Erreur critique Firestore. Vérifiez les règles de sécurité.", e)
|
||||
Log.e("Auth", "Erreur critique Firestore.", e)
|
||||
isAuthorized = false
|
||||
}
|
||||
} else if (firebaseUser != null) {
|
||||
Log.w("Auth", "L'utilisateur est connecté mais son email est vide.")
|
||||
isAuthorized = false
|
||||
}
|
||||
}
|
||||
@@ -132,7 +136,7 @@ fun AuthWrapper(dao: AppDao) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text("Vérification de l'accès...")
|
||||
Text("Vérification et synchronisation...")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,21 +51,29 @@ data class FavoriteMeal(
|
||||
|
||||
@Dao
|
||||
interface AppDao {
|
||||
@Insert suspend fun insertMeal(meal: Meal): Long
|
||||
// On ajoute OnConflictStrategy.REPLACE pour la synchronisation
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertMeal(meal: Meal): Long
|
||||
|
||||
@Delete suspend fun deleteMeal(meal: Meal)
|
||||
@Query("SELECT * FROM meals WHERE date >= :startOfDay AND date < :endOfDay ORDER BY date DESC")
|
||||
fun getMealsForDay(startOfDay: Long, endOfDay: Long): Flow<List<Meal>>
|
||||
@Query("SELECT * FROM meals WHERE date >= :start AND date <= :end ORDER BY date ASC")
|
||||
suspend fun getMealsInRangeSync(start: Long, end: Long): List<Meal>
|
||||
|
||||
@Insert suspend fun insertGlycemia(glycemia: Glycemia): Long
|
||||
// On ajoute OnConflictStrategy.REPLACE pour la synchronisation
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertGlycemia(glycemia: Glycemia): Long
|
||||
|
||||
@Delete suspend fun deleteGlycemia(glycemia: Glycemia)
|
||||
@Query("SELECT * FROM glycemia WHERE date >= :startOfDay AND date < :endOfDay ORDER BY date DESC")
|
||||
fun getGlycemiaForDay(startOfDay: Long, endOfDay: Long): Flow<List<Glycemia>>
|
||||
@Query("SELECT * FROM glycemia WHERE date >= :start AND date <= :end ORDER BY date ASC")
|
||||
suspend fun getGlycemiaInRangeSync(start: Long, end: Long): List<Glycemia>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertSports(sports: List<SportActivity>)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertSports(sports: List<SportActivity>)
|
||||
|
||||
@Query("SELECT * FROM sports ORDER BY date DESC") fun getAllSports(): Flow<List<SportActivity>>
|
||||
@Query("SELECT * FROM sports WHERE date >= :startOfDay AND date < :endOfDay ORDER BY date DESC")
|
||||
fun getSportsForDay(startOfDay: Long, endOfDay: Long): Flow<List<SportActivity>>
|
||||
|
||||
@@ -4,14 +4,13 @@ import android.util.Log
|
||||
import com.google.firebase.auth.FirebaseAuth
|
||||
import com.google.firebase.firestore.FirebaseFirestore
|
||||
import com.google.firebase.firestore.SetOptions
|
||||
import kotlinx.coroutines.tasks.await
|
||||
|
||||
object FirebaseUtils {
|
||||
private fun getDb(): FirebaseFirestore {
|
||||
return try {
|
||||
// Tente d'utiliser la base de données nommée "scan-wich"
|
||||
FirebaseFirestore.getInstance("scan-wich")
|
||||
} catch (e: Exception) {
|
||||
// Repli sur la base par défaut si "scan-wich" n'est pas configurée comme base secondaire
|
||||
FirebaseFirestore.getInstance()
|
||||
}
|
||||
}
|
||||
@@ -48,4 +47,34 @@ object FirebaseUtils {
|
||||
.addOnFailureListener { e -> Log.e("Firestore", "Error sport: ${e.message}") }
|
||||
}
|
||||
}
|
||||
|
||||
// NOUVELLE FONCTION : Rapatrie toutes les données depuis le Cloud
|
||||
suspend fun fetchAllDataFromFirestore(dao: AppDao) {
|
||||
val user = FirebaseAuth.getInstance().currentUser ?: return
|
||||
val db = getDb()
|
||||
val userDoc = db.collection("users").document(user.uid)
|
||||
|
||||
try {
|
||||
// 1. Récupérer les repas
|
||||
val mealsSnapshot = userDoc.collection("meals").get().await()
|
||||
val meals = mealsSnapshot.toObjects(Meal::class.java)
|
||||
meals.forEach { dao.insertMeal(it) }
|
||||
Log.d("FirestoreSync", "${meals.size} repas récupérés")
|
||||
|
||||
// 2. Récupérer la glycémie
|
||||
val glycemiaSnapshot = userDoc.collection("glycemia").get().await()
|
||||
val glycemia = glycemiaSnapshot.toObjects(Glycemia::class.java)
|
||||
glycemia.forEach { dao.insertGlycemia(it) }
|
||||
Log.d("FirestoreSync", "${glycemia.size} glycémies récupérées")
|
||||
|
||||
// 3. Récupérer le sport
|
||||
val sportsSnapshot = userDoc.collection("sports").get().await()
|
||||
val sports = sportsSnapshot.toObjects(SportActivity::class.java)
|
||||
dao.insertSports(sports)
|
||||
Log.d("FirestoreSync", "${sports.size} activités sportives récupérées")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("FirestoreSync", "Erreur lors du rapatriement des données: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.google.firebase.Firebase
|
||||
import com.google.firebase.appcheck.appCheck
|
||||
import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory
|
||||
import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory
|
||||
import com.google.firebase.functions.functions
|
||||
import com.google.firebase.initialize
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -58,19 +59,33 @@ class MainActivity : ComponentActivity() {
|
||||
val code = data.getQueryParameter("code")
|
||||
if (code != null) {
|
||||
val prefs = ApiClient.getEncryptedPrefs(this)
|
||||
val clientId = prefs.getString("strava_client_id", "") ?: ""
|
||||
val clientSecret = prefs.getString("strava_client_secret", "") ?: ""
|
||||
if (clientId.isNotEmpty() && clientSecret.isNotEmpty()) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val response = ApiClient.stravaApi.exchangeToken(clientId, clientSecret, code)
|
||||
val functions = Firebase.functions
|
||||
|
||||
val requestData = hashMapOf(
|
||||
"code" to code
|
||||
)
|
||||
|
||||
functions.getHttpsCallable("exchangeStravaToken")
|
||||
.call(requestData)
|
||||
.addOnSuccessListener { result ->
|
||||
val res = result.data as? Map<*, *>
|
||||
if (res != null) {
|
||||
val accessToken = res["access_token"] as? String
|
||||
val refreshToken = res["refresh_token"] as? String
|
||||
val expiresAt = (res["expires_at"] as? Number)?.toLong() ?: 0L
|
||||
|
||||
if (accessToken != null && refreshToken != null) {
|
||||
prefs.edit {
|
||||
putString("strava_token", response.accessToken)
|
||||
putString("strava_refresh_token", response.refreshToken)
|
||||
putLong("strava_expires_at", response.expiresAt)
|
||||
}
|
||||
} catch (e: Exception) { Log.e("StravaAuth", "Exchange failed: ${e.message}") }
|
||||
putString("strava_token", accessToken)
|
||||
putString("strava_refresh_token", refreshToken)
|
||||
putLong("strava_expires_at", expiresAt)
|
||||
}
|
||||
Log.d("StravaAuth", "Token exchange successful")
|
||||
}
|
||||
}
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Log.e("StravaAuth", "Cloud Function exchange failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
package com.example.scanwich
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.edit
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import com.google.firebase.Firebase
|
||||
import com.google.firebase.functions.functions
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
@@ -13,6 +19,7 @@ import retrofit2.http.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.tasks.await
|
||||
|
||||
// --- OPEN FOOD FACTS API ---
|
||||
data class OffProductResponse(val status: Int, val product: OffProduct?)
|
||||
@@ -64,12 +71,15 @@ object ApiClient {
|
||||
.create(StravaApi::class.java)
|
||||
|
||||
val offApi: OffApi = Retrofit.Builder()
|
||||
.baseUrl("https://world.openfoodfacts.org/api/v2/")
|
||||
// On force l'utilisation de l'API française pour les codes-barres
|
||||
.baseUrl("https://fr.openfoodfacts.org/api/v2/")
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
.create(OffApi::class.java)
|
||||
|
||||
private const val STRAVA_CLIENT_ID = "203805"
|
||||
|
||||
fun getEncryptedPrefs(context: Context): SharedPreferences {
|
||||
val masterKey = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
|
||||
return EncryptedSharedPreferences.create(
|
||||
@@ -84,19 +94,38 @@ object ApiClient {
|
||||
suspend fun getValidStravaToken(prefs: SharedPreferences): String? {
|
||||
val stravaToken = prefs.getString("strava_token", null) ?: return null
|
||||
val expiresAt = prefs.getLong("strava_expires_at", 0)
|
||||
if (System.currentTimeMillis() / 1000 >= expiresAt) {
|
||||
|
||||
if (System.currentTimeMillis() / 1000 >= (expiresAt - 300)) {
|
||||
val refreshToken = prefs.getString("strava_refresh_token", null) ?: return null
|
||||
val clientId = prefs.getString("strava_client_id", "") ?: ""
|
||||
val clientSecret = prefs.getString("strava_client_secret", "") ?: ""
|
||||
|
||||
return try {
|
||||
val res = stravaApi.refreshToken(clientId, clientSecret, refreshToken)
|
||||
val functions = Firebase.functions
|
||||
val data = hashMapOf("refreshToken" to refreshToken)
|
||||
|
||||
val result = functions.getHttpsCallable("refreshStravaToken")
|
||||
.call(data)
|
||||
.await()
|
||||
|
||||
val res = result.data as? Map<*, *>
|
||||
if (res != null) {
|
||||
val newAccessToken = res["access_token"] as? String
|
||||
val newRefreshToken = res["refresh_token"] as? String
|
||||
val newExpiresAt = (res["expires_at"] as? Number)?.toLong() ?: 0L
|
||||
|
||||
if (newAccessToken != null && newRefreshToken != null) {
|
||||
prefs.edit {
|
||||
putString("strava_token", res.accessToken)
|
||||
putString("strava_refresh_token", res.refreshToken)
|
||||
putLong("strava_expires_at", res.expiresAt)
|
||||
putString("strava_token", newAccessToken)
|
||||
putString("strava_refresh_token", newRefreshToken)
|
||||
putLong("strava_expires_at", newExpiresAt)
|
||||
}
|
||||
return newAccessToken
|
||||
}
|
||||
}
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
Log.e("StravaAuth", "Refresh failed: ${e.message}")
|
||||
null
|
||||
}
|
||||
res.accessToken
|
||||
} catch (e: Exception) { null }
|
||||
}
|
||||
return stravaToken
|
||||
}
|
||||
@@ -125,4 +154,18 @@ object ApiClient {
|
||||
val durationHours = activity.movingTime / 3600.0
|
||||
return met * weightKg * durationHours
|
||||
}
|
||||
|
||||
fun launchStravaAuth(context: Context) {
|
||||
val intentUri = Uri.parse("https://www.strava.com/oauth/mobile/authorize")
|
||||
.buildUpon()
|
||||
.appendQueryParameter("client_id", STRAVA_CLIENT_ID)
|
||||
.appendQueryParameter("redirect_uri", "coloricam://localhost")
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.appendQueryParameter("approval_prompt", "auto")
|
||||
.appendQueryParameter("scope", "activity:read_all")
|
||||
.build()
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW, intentUri)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.example.scanwich
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
@@ -16,16 +14,27 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(prefs: SharedPreferences, onLogout: () -> Unit, onProfileUpdated: () -> Unit) {
|
||||
var isEditing by remember { mutableStateOf(false) }
|
||||
val context = LocalContext.current
|
||||
|
||||
var stravaClientId by remember { mutableStateOf(prefs.getString("strava_client_id", "") ?: "") }
|
||||
var stravaClientSecret by remember { mutableStateOf(prefs.getString("strava_client_secret", "") ?: "") }
|
||||
val isStravaConnected = prefs.contains("strava_token")
|
||||
// État réactif pour la connexion Strava
|
||||
var isStravaConnected by remember { mutableStateOf(prefs.contains("strava_token")) }
|
||||
|
||||
// Écouteur pour mettre à jour l'état si le token change
|
||||
DisposableEffect(prefs) {
|
||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { p, key ->
|
||||
if (key == "strava_token") {
|
||||
isStravaConnected = p.contains("strava_token")
|
||||
}
|
||||
}
|
||||
prefs.registerOnSharedPreferenceChangeListener(listener)
|
||||
onDispose {
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
SetupScreen(prefs) {
|
||||
@@ -48,51 +57,25 @@ fun SettingsScreen(prefs: SharedPreferences, onLogout: () -> Unit, onProfileUpda
|
||||
Button(onClick = { isEditing = true }, modifier = Modifier.fillMaxWidth()) { Icon(Icons.Default.Edit, null); Text(" Modifier le profil") }
|
||||
|
||||
Spacer(Modifier.height(32.dp))
|
||||
Text("Configuration Strava", style = MaterialTheme.typography.titleMedium)
|
||||
Text("Intégrations", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = stravaClientId,
|
||||
onValueChange = { stravaClientId = it; prefs.edit { putString("strava_client_id", it) } },
|
||||
label = { Text("Strava Client ID") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = stravaClientSecret,
|
||||
onValueChange = { stravaClientSecret = it; prefs.edit { putString("strava_client_secret", it) } },
|
||||
label = { Text("Strava Client Secret") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
if (!isStravaConnected) {
|
||||
Button(
|
||||
onClick = {
|
||||
if (stravaClientId.isBlank() || stravaClientSecret.isBlank()) {
|
||||
Toast.makeText(context, "Entrez votre ID et Secret Client", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
val intent = Intent(Intent.ACTION_VIEW, "https://www.strava.com/oauth/mobile/authorize?client_id=$stravaClientId&redirect_uri=coloricam://localhost&response_type=code&approval_prompt=auto&scope=read,activity:read_all".toUri())
|
||||
context.startActivity(intent)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Connecter Strava")
|
||||
}
|
||||
} else {
|
||||
if (isStravaConnected) {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
prefs.edit {
|
||||
remove("strava_token")
|
||||
remove("strava_refresh_token")
|
||||
}
|
||||
// L'écouteur mettra `isStravaConnected` à jour automatiquement
|
||||
Toast.makeText(context, "Strava déconnecté", Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Déconnecter Strava (Connecté)")
|
||||
}
|
||||
} else {
|
||||
Text("Aucune intégration active. Allez dans l'onglet 'Sport' pour connecter Strava.", style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(32.dp))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.example.scanwich
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -8,6 +9,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Sync
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -23,21 +25,51 @@ import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@Composable
|
||||
fun SportScreen(dao: AppDao, prefs: android.content.SharedPreferences) {
|
||||
fun SportScreen(dao: AppDao, prefs: SharedPreferences) {
|
||||
val sports by dao.getAllSports().collectAsState(initial = emptyList())
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
// État réactif pour la connexion Strava
|
||||
var isConnectedToStrava by remember {
|
||||
mutableStateOf(prefs.getString("strava_token", null) != null)
|
||||
}
|
||||
|
||||
// Écouteur de changements pour les SharedPreferences
|
||||
DisposableEffect(prefs) {
|
||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { p, key ->
|
||||
if (key == "strava_token") {
|
||||
isConnectedToStrava = p.getString("strava_token", null) != null
|
||||
}
|
||||
}
|
||||
prefs.registerOnSharedPreferenceChangeListener(listener)
|
||||
onDispose {
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
Text("Activités Sportives", style = MaterialTheme.typography.headlineMedium)
|
||||
|
||||
if (!isConnectedToStrava) {
|
||||
Button(
|
||||
onClick = { ApiClient.launchStravaAuth(context) },
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFC6100)) // Strava orange
|
||||
) {
|
||||
Icon(Icons.Default.Sync, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Se connecter à Strava")
|
||||
}
|
||||
} else {
|
||||
Button(
|
||||
onClick = { syncStravaActivities(dao, prefs, coroutineScope, context) },
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Refresh, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Synchroniser Strava")
|
||||
Text("Synchroniser les activités")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
@@ -50,7 +82,8 @@ fun SportScreen(dao: AppDao, prefs: android.content.SharedPreferences) {
|
||||
Text(activity.name, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
|
||||
Text(SimpleDateFormat("dd/MM HH:mm", Locale.getDefault()).format(Date(activity.date)), style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
Text("${activity.type} - ${(activity.distance / 1000).format(2)} km")
|
||||
val distanceInKm = activity.distance / 1000
|
||||
Text("${activity.type} - ${String.format(Locale.getDefault(), "%.2f", distanceInKm)} km")
|
||||
Text("${activity.calories?.toInt() ?: 0} kcal brûlées", color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
@@ -59,11 +92,11 @@ fun SportScreen(dao: AppDao, prefs: android.content.SharedPreferences) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncStravaActivities(dao: AppDao, prefs: android.content.SharedPreferences, scope: CoroutineScope, context: Context) {
|
||||
private fun syncStravaActivities(dao: AppDao, prefs: SharedPreferences, scope: CoroutineScope, context: Context) {
|
||||
scope.launch {
|
||||
val token = ApiClient.getValidStravaToken(prefs)
|
||||
if (token == null) {
|
||||
Toast.makeText(context, "Veuillez connecter Strava dans les paramètres", Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(context, "Erreur de connexion Strava", Toast.LENGTH_LONG).show()
|
||||
return@launch
|
||||
}
|
||||
try {
|
||||
@@ -83,7 +116,7 @@ private fun syncStravaActivities(dao: AppDao, prefs: android.content.SharedPrefe
|
||||
).toFloat(),
|
||||
date = ApiClient.parseStravaDate(it.startDate)
|
||||
)
|
||||
syncSportToFirestore(activity) // Firestore Sync
|
||||
syncSportToFirestore(activity)
|
||||
activity
|
||||
}
|
||||
dao.insertSports(sportActivities)
|
||||
|
||||
@@ -2,48 +2,49 @@
|
||||
|
||||
**Changements majeurs de la version actuelle :**
|
||||
|
||||
🎨 **Améliorations de l'Interface Utilisateur :**
|
||||
- **Correction du Thème :** Résolution d'un problème de contraste sur l'écran de configuration du profil qui rendait le texte illisible (texte jaune sur fond blanc). L'écran respecte désormais correctement le thème de l'application.
|
||||
🇫🇷 **Expérience 100% en Français :**
|
||||
- **IA Francophone :** Les analyses de repas (caméra et manuel) sont désormais exclusivement en français grâce à des instructions renforcées côté serveur.
|
||||
- **Scanner Localisé :** Utilisation forcée de la base de données française d'Open Food Facts pour le scan de codes-barres, garantissant des noms de produits familiers.
|
||||
|
||||
🛡️ **Refonte de l'Architecture de Sécurité et de Données :**
|
||||
- **Gestion des accès centralisée :** L'ancien système d'utilisateurs autorisés codé en dur dans l'application a été supprimé. L'accès est désormais contrôlé de manière sécurisée et dynamique via une liste d'autorisation sur le serveur Firebase Firestore. Cela ouvre la voie à la gestion d'abonnements.
|
||||
- **Profils Utilisateurs dans le Cloud :** Les données de profil (poids, objectifs, etc.) sont maintenant synchronisées avec le compte Firebase de l'utilisateur, permettant une expérience cohérente sur plusieurs appareils.
|
||||
- **Correctif Critique de Connexion :** Résolution d'un bug majeur qui empêchait l'application de se connecter correctement à la base de données Firestore, causant des accès refusés inattendus pour les utilisateurs légitimes. L'application est désormais compatible avec les bases de données Firestore nommées.
|
||||
🤖 **Analyse IA plus Robuste :**
|
||||
- **Correctif d'analyse :** Résolution du bug "Erreur IA" lié au formatage des réponses du modèle.
|
||||
- **Fiabilité accrue :** Nettoyage automatique des données nutritionnelles côté serveur (Cloud Functions).
|
||||
|
||||
🚀 **Connexion Strava 100% Automatique :**
|
||||
- **Simplicité totale :** Connexion en un seul clic sans saisie d'identifiants techniques.
|
||||
- **Sécurité Maximale :** Utilisation de Google Cloud Secret Manager pour la protection des clés API Strava.
|
||||
|
||||
🎨 **Améliorations UI/UX :**
|
||||
- **Interface épurée :** Suppression des réglages superflus.
|
||||
- **Correction du Thème :** Lisibilité parfaite sur l'écran de profil.
|
||||
|
||||
🛡️ **Architecture Cloud :**
|
||||
- **Gestion des accès :** Contrôle dynamique des utilisateurs autorisés via Firestore.
|
||||
- **Profils Cloud :** Synchronisation automatique de vos données personnelles.
|
||||
|
||||
---
|
||||
|
||||
**Mises à jour précédentes :**
|
||||
|
||||
🛠️ **Correctifs et Améliorations Strava :**
|
||||
- Résolution d'un problème de compilation bloquant sur l'écran des sports.
|
||||
- Intégration d'un nouvel algorithme d'estimation des calories basé sur les MET (Metabolic Equivalent of Task) pour une précision accrue.
|
||||
- Amélioration de la fiabilité du parsing des dates d'activités Strava.
|
||||
- Résolution de bugs de compilation et amélioration du parsing des dates.
|
||||
- Nouvel algorithme d'estimation des calories basé sur les MET.
|
||||
|
||||
🛡️ **Sécurité renforcée :**
|
||||
- Intégration de Firebase App Check (Play Integrity) pour protéger l'API contre les accès non autorisés.
|
||||
- Migration de la clé API vers Google Cloud Secret Manager, supprimant toute information sensible du code source.
|
||||
- Intégration de Firebase App Check (Play Integrity).
|
||||
- Migration des clés vers Secret Manager.
|
||||
|
||||
⚡ **Analyse Ultra-Rapide :**
|
||||
- Nouveau moteur de compression d'image intelligent (réduction de ~2.2 Mo à 150 Ko par scan), accélérant drastiquement l'analyse IA.
|
||||
|
||||
🤖 **IA Sécurisée :**
|
||||
- Migration de la logique d'analyse (prompts) vers des Cloud Functions pour garantir des résultats plus fiables et protégés.
|
||||
- Nouveau moteur de compression d'image intelligent.
|
||||
|
||||
📄 **Export PDF Professionnel :**
|
||||
- Nouvelle fonctionnalité d'exportation de l'historique. Générez un rapport PDF complet incluant vos repas, activités sportives et suivis de glycémie.
|
||||
- Exportation de l'historique complet (repas, sport, glycémie).
|
||||
|
||||
🩸 **Suivi Diabétique complet :**
|
||||
- Possibilité d'enregistrer et de visualiser sa glycémie avant/après chaque catégorie de repas directement dans l'historique.
|
||||
|
||||
🚴 **Synchronisation Strava :**
|
||||
- Connexion directe à Strava pour importer vos activités et calculer précisément les calories brûlées.
|
||||
🩸 **Suivi Diabétique :**
|
||||
- Visualisation de la glycémie directement dans l'historique.
|
||||
|
||||
⭐ **Gestion des Favoris :**
|
||||
- Refonte de l'interface des favoris avec une nouvelle fenêtre modale pour un ajout rapide de vos repas récurrents.
|
||||
- Nouvelle interface d'ajout rapide.
|
||||
|
||||
🔍 **Scanner de Code-barres :**
|
||||
- Intégration d'Open Food Facts pour identifier instantanément les produits industriels via leur code-barres.
|
||||
|
||||
🔧 **Stabilité et Modernisation :**
|
||||
- Optimisation pour Android 36.
|
||||
- Correction de bugs majeurs sur le stockage sécurisé des préférences (MasterKey) et la synchronisation Firebase (collectAsState).
|
||||
🔧 **Stabilité :**
|
||||
- Optimisation pour Android 36 et corrections diverses.
|
||||
|
||||
Reference in New Issue
Block a user