Add camera and storage permissions, and implement profile picture upload functionality
This commit is contained in:
@@ -5,6 +5,15 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|
||||||
|
<!-- Permissions pour la galerie et les fichiers -->
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
|
|
||||||
|
<!-- Permissions pour écrire dans le stockage -->
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<application
|
<application
|
||||||
android:label="travel_mate"
|
android:label="travel_mate"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
|||||||
@@ -73,5 +73,16 @@
|
|||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
|
<!-- Permission Caméra -->
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>L'application a besoin d'accéder à votre caméra pour prendre des photos de profil.</string>
|
||||||
|
|
||||||
|
<!-- Permission Galerie -->
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>L'application a besoin d'accéder à votre galerie pour sélectionner une photo de profil.</string>
|
||||||
|
|
||||||
|
<!-- Permission pour ajouter à la galerie (iOS 14+) -->
|
||||||
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
|
<string>L'application a besoin de sauvegarder vos photos.</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -78,10 +78,13 @@ class UserModel {
|
|||||||
/// User's phone number (optional).
|
/// User's phone number (optional).
|
||||||
final String? phoneNumber;
|
final String? phoneNumber;
|
||||||
|
|
||||||
|
/// User's profile picture URL (optional).
|
||||||
|
final String? profilePictureUrl;
|
||||||
|
|
||||||
/// Creates a new [UserModel] instance.
|
/// Creates a new [UserModel] instance.
|
||||||
///
|
///
|
||||||
/// [id], [email], and [prenom] are required fields.
|
/// [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({
|
UserModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.email,
|
required this.email,
|
||||||
@@ -89,6 +92,7 @@ class UserModel {
|
|||||||
this.nom,
|
this.nom,
|
||||||
this.authMethod,
|
this.authMethod,
|
||||||
this.phoneNumber,
|
this.phoneNumber,
|
||||||
|
this.profilePictureUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Creates a [UserModel] instance from a JSON map.
|
/// Creates a [UserModel] instance from a JSON map.
|
||||||
@@ -103,6 +107,7 @@ class UserModel {
|
|||||||
nom: json['nom'],
|
nom: json['nom'],
|
||||||
authMethod: json['authMethod'] ?? json['platform'],
|
authMethod: json['authMethod'] ?? json['platform'],
|
||||||
phoneNumber: json['phoneNumber'],
|
phoneNumber: json['phoneNumber'],
|
||||||
|
profilePictureUrl: json['profilePictureUrl'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +122,7 @@ class UserModel {
|
|||||||
'nom': nom,
|
'nom': nom,
|
||||||
'authMethod': authMethod,
|
'authMethod': authMethod,
|
||||||
'phoneNumber': phoneNumber,
|
'phoneNumber': phoneNumber,
|
||||||
|
'profilePictureUrl': profilePictureUrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:travel_mate/components/widgets/user_state_widget.dart';
|
import 'package:travel_mate/components/widgets/user_state_widget.dart';
|
||||||
import 'package:travel_mate/services/error_service.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_bloc.dart';
|
||||||
import '../../../blocs/user/user_state.dart' as user_state;
|
import '../../../blocs/user/user_state.dart' as user_state;
|
||||||
import '../../../blocs/user/user_event.dart' as user_event;
|
import '../../../blocs/user/user_event.dart' as user_event;
|
||||||
@@ -40,20 +43,32 @@ class ProfileContent extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: CircleAvatar(
|
child: user.profilePictureUrl != null && user.profilePictureUrl!.isNotEmpty
|
||||||
radius: 50,
|
? CircleAvatar(
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
radius: 50,
|
||||||
child: Text(
|
backgroundImage: NetworkImage(
|
||||||
user.prenom.isNotEmpty
|
user.profilePictureUrl!,
|
||||||
? user.prenom[0].toUpperCase()
|
// Force le rechargement avec un paramètre de cache
|
||||||
: 'U',
|
headers: {
|
||||||
style: TextStyle(
|
'pragma': 'no-cache',
|
||||||
fontSize: 32,
|
'cache-control': 'no-cache',
|
||||||
fontWeight: FontWeight.bold,
|
},
|
||||||
color: Colors.white,
|
),
|
||||||
),
|
)
|
||||||
),
|
: 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),
|
SizedBox(height: 16),
|
||||||
@@ -367,58 +382,287 @@ class ProfileContent extends StatelessWidget {
|
|||||||
void _showEditProfileDialog(BuildContext context, user_state.UserModel user) {
|
void _showEditProfileDialog(BuildContext context, user_state.UserModel user) {
|
||||||
final nomController = TextEditingController(text: user.nom);
|
final nomController = TextEditingController(text: user.nom);
|
||||||
final prenomController = TextEditingController(text: user.prenom);
|
final prenomController = TextEditingController(text: user.prenom);
|
||||||
|
final phoneController = TextEditingController(text: user.phoneNumber ?? '');
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext dialogContext) {
|
builder: (BuildContext dialogContext) {
|
||||||
return AlertDialog(
|
return StatefulBuilder(
|
||||||
title: Text('Modifier le profil'),
|
builder: (context, setState) {
|
||||||
content: Column(
|
return AlertDialog(
|
||||||
mainAxisSize: MainAxisSize.min,
|
title: Text('Modifier le profil'),
|
||||||
children: [
|
content: SingleChildScrollView(
|
||||||
TextField(
|
child: Column(
|
||||||
controller: prenomController,
|
mainAxisSize: MainAxisSize.min,
|
||||||
decoration: InputDecoration(labelText: 'Prénom'),
|
children: [
|
||||||
),
|
// Photo de profil avec option de changement
|
||||||
SizedBox(height: 16),
|
Center(
|
||||||
TextField(
|
child: Stack(
|
||||||
controller: nomController,
|
children: [
|
||||||
decoration: InputDecoration(labelText: 'Nom'),
|
CircleAvatar(
|
||||||
),
|
radius: 50,
|
||||||
],
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
),
|
child: Text(
|
||||||
actions: [
|
prenomController.text.isNotEmpty
|
||||||
TextButton(
|
? prenomController.text[0].toUpperCase()
|
||||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
: 'U',
|
||||||
child: Text('Annuler'),
|
style: TextStyle(
|
||||||
),
|
fontSize: 32,
|
||||||
TextButton(
|
fontWeight: FontWeight.bold,
|
||||||
onPressed: () {
|
color: Colors.white,
|
||||||
if (prenomController.text.trim().isNotEmpty) {
|
),
|
||||||
context.read<UserBloc>().add(
|
),
|
||||||
user_event.UserUpdated({
|
),
|
||||||
'prenom': prenomController.text.trim(),
|
Positioned(
|
||||||
'nom': nomController.text.trim(),
|
bottom: 0,
|
||||||
}),
|
right: 0,
|
||||||
);
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
Navigator.of(dialogContext).pop();
|
shape: BoxShape.circle,
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
color: Theme.of(context).colorScheme.primary,
|
||||||
SnackBar(
|
),
|
||||||
content: Text('Profil mis à jour !'),
|
child: IconButton(
|
||||||
backgroundColor: Colors.green,
|
icon: Icon(Icons.camera_alt, color: Colors.white, size: 20),
|
||||||
|
onPressed: () {
|
||||||
|
_showPhotoPickerDialog(dialogContext);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
SizedBox(height: 24),
|
||||||
}
|
|
||||||
},
|
// Champ Prénom
|
||||||
child: Text('Sauvegarder'),
|
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(
|
void _showChangePasswordDialog(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
user_state.UserModel user,
|
user_state.UserModel user,
|
||||||
|
|||||||
Reference in New Issue
Block a user