diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index a0bd446..d4a4263 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -5,6 +5,15 @@
+
+
+
+
+
+
+
+
+
+
+ NSCameraUsageDescription
+ L'application a besoin d'accéder à votre caméra pour prendre des photos de profil.
+
+
+ NSPhotoLibraryUsageDescription
+ L'application a besoin d'accéder à votre galerie pour sélectionner une photo de profil.
+
+
+ NSPhotoLibraryAddUsageDescription
+ L'application a besoin de sauvegarder vos photos.
diff --git a/lib/blocs/user/user_state.dart b/lib/blocs/user/user_state.dart
index c32f062..a3d3601 100644
--- a/lib/blocs/user/user_state.dart
+++ b/lib/blocs/user/user_state.dart
@@ -78,10 +78,13 @@ class UserModel {
/// User's phone number (optional).
final String? phoneNumber;
+ /// User's profile picture URL (optional).
+ final String? profilePictureUrl;
+
/// Creates a new [UserModel] instance.
///
/// [id], [email], and [prenom] are required fields.
- /// [nom], [authMethod], and [phoneNumber] are optional and can be null.
+ /// [nom], [authMethod], [phoneNumber], and [profilePictureUrl] are optional and can be null.
UserModel({
required this.id,
required this.email,
@@ -89,6 +92,7 @@ class UserModel {
this.nom,
this.authMethod,
this.phoneNumber,
+ this.profilePictureUrl,
});
/// Creates a [UserModel] instance from a JSON map.
@@ -103,6 +107,7 @@ class UserModel {
nom: json['nom'],
authMethod: json['authMethod'] ?? json['platform'],
phoneNumber: json['phoneNumber'],
+ profilePictureUrl: json['profilePictureUrl'],
);
}
@@ -117,6 +122,7 @@ class UserModel {
'nom': nom,
'authMethod': authMethod,
'phoneNumber': phoneNumber,
+ 'profilePictureUrl': profilePictureUrl,
};
}
}
diff --git a/lib/components/settings/profile/profile_content.dart b/lib/components/settings/profile/profile_content.dart
index ab19a7b..53fca14 100644
--- a/lib/components/settings/profile/profile_content.dart
+++ b/lib/components/settings/profile/profile_content.dart
@@ -2,6 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:travel_mate/components/widgets/user_state_widget.dart';
import 'package:travel_mate/services/error_service.dart';
+import 'package:image_picker/image_picker.dart';
+import 'package:firebase_storage/firebase_storage.dart';
+import 'dart:io';
import '../../../blocs/user/user_bloc.dart';
import '../../../blocs/user/user_state.dart' as user_state;
import '../../../blocs/user/user_event.dart' as user_event;
@@ -40,20 +43,32 @@ class ProfileContent extends StatelessWidget {
)
],
),
- child: CircleAvatar(
- radius: 50,
- backgroundColor: Theme.of(context).colorScheme.primary,
- child: Text(
- user.prenom.isNotEmpty
- ? user.prenom[0].toUpperCase()
- : 'U',
- style: TextStyle(
- fontSize: 32,
- fontWeight: FontWeight.bold,
- color: Colors.white,
- ),
- ),
- ),
+ child: user.profilePictureUrl != null && user.profilePictureUrl!.isNotEmpty
+ ? CircleAvatar(
+ radius: 50,
+ backgroundImage: NetworkImage(
+ user.profilePictureUrl!,
+ // Force le rechargement avec un paramètre de cache
+ headers: {
+ 'pragma': 'no-cache',
+ 'cache-control': 'no-cache',
+ },
+ ),
+ )
+ : CircleAvatar(
+ radius: 50,
+ backgroundColor: Theme.of(context).colorScheme.primary,
+ child: Text(
+ user.prenom.isNotEmpty
+ ? user.prenom[0].toUpperCase()
+ : 'U',
+ style: TextStyle(
+ fontSize: 32,
+ fontWeight: FontWeight.bold,
+ color: Colors.white,
+ ),
+ ),
+ ),
),
SizedBox(height: 16),
@@ -367,58 +382,287 @@ class ProfileContent extends StatelessWidget {
void _showEditProfileDialog(BuildContext context, user_state.UserModel user) {
final nomController = TextEditingController(text: user.nom);
final prenomController = TextEditingController(text: user.prenom);
+ final phoneController = TextEditingController(text: user.phoneNumber ?? '');
showDialog(
context: context,
builder: (BuildContext dialogContext) {
- return AlertDialog(
- title: Text('Modifier le profil'),
- content: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- TextField(
- controller: prenomController,
- decoration: InputDecoration(labelText: 'Prénom'),
- ),
- SizedBox(height: 16),
- TextField(
- controller: nomController,
- decoration: InputDecoration(labelText: 'Nom'),
- ),
- ],
- ),
- actions: [
- TextButton(
- onPressed: () => Navigator.of(dialogContext).pop(),
- child: Text('Annuler'),
- ),
- TextButton(
- onPressed: () {
- if (prenomController.text.trim().isNotEmpty) {
- context.read().add(
- user_event.UserUpdated({
- 'prenom': prenomController.text.trim(),
- 'nom': nomController.text.trim(),
- }),
- );
-
- Navigator.of(dialogContext).pop();
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text('Profil mis à jour !'),
- backgroundColor: Colors.green,
+ return StatefulBuilder(
+ builder: (context, setState) {
+ return AlertDialog(
+ title: Text('Modifier le profil'),
+ content: SingleChildScrollView(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ // Photo de profil avec option de changement
+ Center(
+ child: Stack(
+ children: [
+ CircleAvatar(
+ radius: 50,
+ backgroundColor: Theme.of(context).colorScheme.primary,
+ child: Text(
+ prenomController.text.isNotEmpty
+ ? prenomController.text[0].toUpperCase()
+ : 'U',
+ style: TextStyle(
+ fontSize: 32,
+ fontWeight: FontWeight.bold,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ Positioned(
+ bottom: 0,
+ right: 0,
+ child: Container(
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ child: IconButton(
+ icon: Icon(Icons.camera_alt, color: Colors.white, size: 20),
+ onPressed: () {
+ _showPhotoPickerDialog(dialogContext);
+ },
+ ),
+ ),
+ ),
+ ],
+ ),
),
- );
- }
- },
- child: Text('Sauvegarder'),
- ),
- ],
+ SizedBox(height: 24),
+
+ // Champ Prénom
+ TextField(
+ controller: prenomController,
+ decoration: InputDecoration(
+ labelText: 'Prénom',
+ border: OutlineInputBorder(),
+ prefixIcon: Icon(Icons.person_outline),
+ ),
+ onChanged: (_) {
+ setState(() {});
+ },
+ ),
+ SizedBox(height: 16),
+
+ // Champ Nom
+ TextField(
+ controller: nomController,
+ decoration: InputDecoration(
+ labelText: 'Nom',
+ border: OutlineInputBorder(),
+ prefixIcon: Icon(Icons.person_outline),
+ ),
+ ),
+ SizedBox(height: 16),
+
+ // Champ Téléphone
+ TextField(
+ controller: phoneController,
+ decoration: InputDecoration(
+ labelText: 'Numéro de téléphone',
+ border: OutlineInputBorder(),
+ prefixIcon: Icon(Icons.phone_outlined),
+ hintText: '+33 6 12 34 56 78',
+ ),
+ keyboardType: TextInputType.phone,
+ ),
+ ],
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(dialogContext).pop(),
+ child: Text('Annuler'),
+ ),
+ TextButton(
+ onPressed: () {
+ if (prenomController.text.trim().isNotEmpty) {
+ context.read().add(
+ user_event.UserUpdated({
+ 'prenom': prenomController.text.trim(),
+ 'nom': nomController.text.trim(),
+ 'phoneNumber': phoneController.text.trim(),
+ }),
+ );
+
+ Navigator.of(dialogContext).pop();
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Profil mis à jour !'),
+ backgroundColor: Colors.green,
+ ),
+ );
+ }
+ },
+ child: Text('Sauvegarder'),
+ ),
+ ],
+ );
+ },
);
},
);
}
+ void _showPhotoPickerDialog(BuildContext context) {
+ // Récupérer les références AVANT que le modal ne se ferme
+ final userBloc = context.read();
+
+ showModalBottomSheet(
+ context: context,
+ builder: (BuildContext sheetContext) {
+ return Container(
+ child: Wrap(
+ children: [
+ ListTile(
+ leading: Icon(Icons.photo_library),
+ title: Text('Galerie'),
+ onTap: () {
+ Navigator.pop(sheetContext);
+ _pickImageFromGallery(context, userBloc);
+ },
+ ),
+ ListTile(
+ leading: Icon(Icons.camera_alt),
+ title: Text('Caméra'),
+ onTap: () {
+ Navigator.pop(sheetContext);
+ _pickImageFromCamera(context, userBloc);
+ },
+ ),
+ ListTile(
+ leading: Icon(Icons.close),
+ title: Text('Annuler'),
+ onTap: () => Navigator.pop(sheetContext),
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+
+ Future _pickImageFromGallery(BuildContext context, UserBloc userBloc) async {
+ try {
+ final ImagePicker picker = ImagePicker();
+ final XFile? image = await picker.pickImage(source: ImageSource.gallery);
+
+ if (image != null) {
+ await _uploadProfilePicture(context, image.path, userBloc);
+ }
+ } catch (e) {
+ _errorService.showError(message: 'Erreur lors de la sélection de l\'image');
+ }
+ }
+
+ Future _pickImageFromCamera(BuildContext context, UserBloc userBloc) async {
+ try {
+ final ImagePicker picker = ImagePicker();
+ final XFile? image = await picker.pickImage(source: ImageSource.camera);
+
+ if (image != null) {
+ await _uploadProfilePicture(context, image.path, userBloc);
+ }
+ } catch (e) {
+ _errorService.showError(message: 'Erreur lors de la prise de photo');
+ }
+ }
+
+ Future _uploadProfilePicture(BuildContext context, String imagePath, UserBloc userBloc) async {
+ try {
+ final File imageFile = File(imagePath);
+
+ // Vérifier que le fichier existe
+ if (!await imageFile.exists()) {
+ _errorService.showError(message: 'Le fichier image n\'existe pas');
+ return;
+ }
+
+ print('DEBUG: Taille du fichier: ${imageFile.lengthSync()} bytes');
+
+ final userState = userBloc.state;
+ if (userState is! user_state.UserLoaded) {
+ _errorService.showError(message: 'Utilisateur non connecté');
+ return;
+ }
+
+ final user = userState.user;
+
+ // Créer un nom unique pour la photo
+ final String fileName = 'profile_${user.id}_${DateTime.now().millisecondsSinceEpoch}.jpg';
+ final Reference storageRef = FirebaseStorage.instance
+ .ref()
+ .child('profile_pictures')
+ .child(fileName);
+
+ print('DEBUG: Chemin Storage: ${storageRef.fullPath}');
+ print('DEBUG: Upload en cours pour $fileName');
+
+ // Uploader l'image avec gestion d'erreur détaillée
+ try {
+ final uploadTask = storageRef.putFile(imageFile);
+
+ // Écouter la progression
+ uploadTask.snapshotEvents.listen((TaskSnapshot snapshot) {
+ print('DEBUG: Progression: ${snapshot.bytesTransferred}/${snapshot.totalBytes}');
+ });
+
+ final snapshot = await uploadTask;
+ print('DEBUG: Upload terminé. État: ${snapshot.state}');
+ } on FirebaseException catch (e) {
+ print('DEBUG: FirebaseException lors de l\'upload: ${e.code} - ${e.message}');
+ if (context.mounted) {
+ _errorService.showError(
+ message: 'Erreur Firebase: ${e.code}\n${e.message}',
+ );
+ }
+ return;
+ }
+
+ print('DEBUG: Upload terminé, récupération de l\'URL');
+
+ // Récupérer l'URL
+ final String downloadUrl = await storageRef.getDownloadURL();
+
+ print('DEBUG: URL obtenue: $downloadUrl');
+
+ // Mettre à jour le profil avec l'URL en utilisant la référence sauvegardée du BLoC
+ print('DEBUG: Envoi de UserUpdated event au BLoC');
+ userBloc.add(
+ user_event.UserUpdated({
+ 'profilePictureUrl': downloadUrl,
+ }),
+ );
+
+ // Attendre un peu que Firestore se mette à jour
+ await Future.delayed(Duration(milliseconds: 500));
+
+ if (context.mounted) {
+ print('DEBUG: Affichage du succès');
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Photo de profil mise à jour !'),
+ backgroundColor: Colors.green,
+ ),
+ );
+ }
+ } catch (e, stackTrace) {
+ print('DEBUG: Erreur lors de l\'upload: $e');
+ print('DEBUG: Stack trace: $stackTrace');
+ _errorService.logError(
+ 'ProfileContent - _uploadProfilePicture',
+ 'Erreur lors de l\'upload de la photo: $e\n$stackTrace',
+ );
+ if (context.mounted) {
+ _errorService.showError(message: 'Erreur: ${e.toString()}');
+ }
+ }
+ }
+
void _showChangePasswordDialog(
BuildContext context,
user_state.UserModel user,