From 3560b2d6f5889e8a29336c340e1790792d9db33d Mon Sep 17 00:00:00 2001 From: Van Leemput Dayron Date: Thu, 13 Nov 2025 15:33:43 +0100 Subject: [PATCH] Add camera and storage permissions, and implement profile picture upload functionality --- android/app/src/main/AndroidManifest.xml | 9 + ios/Runner/Info.plist | 11 + lib/blocs/user/user_state.dart | 8 +- .../settings/profile/profile_content.dart | 356 +++++++++++++++--- 4 files changed, 327 insertions(+), 57 deletions(-) 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,