Add camera and storage permissions, and implement profile picture upload functionality
This commit is contained in:
@@ -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<UserBloc>().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<UserBloc>().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<UserBloc>();
|
||||
|
||||
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<void> _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<void> _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<void> _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,
|
||||
|
||||
Reference in New Issue
Block a user