diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 26b0936..f536956 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ + diff --git a/devtools_options.yaml b/devtools_options.yaml index fa0b357..2bc8e05 100644 --- a/devtools_options.yaml +++ b/devtools_options.yaml @@ -1,3 +1,4 @@ description: This file stores settings for Dart & Flutter DevTools. documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states extensions: + - provider: true \ No newline at end of file diff --git a/flutter_01.png b/flutter_01.png deleted file mode 100644 index e69de29..0000000 diff --git a/flutter_02.png b/flutter_02.png deleted file mode 100644 index f5cd612..0000000 Binary files a/flutter_02.png and /dev/null differ diff --git a/flutter_03.png b/flutter_03.png deleted file mode 100644 index 8ba7e4d..0000000 Binary files a/flutter_03.png and /dev/null differ diff --git a/flutter_04.png b/flutter_04.png deleted file mode 100644 index c45b299..0000000 Binary files a/flutter_04.png and /dev/null differ diff --git a/lib/assets/animations/friend_expanding_card.dart b/lib/assets/animations/friend_expanding_card.dart new file mode 100644 index 0000000..1827f43 --- /dev/null +++ b/lib/assets/animations/friend_expanding_card.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; + +/// [FriendExpandingCard] est un widget animé qui s'agrandit pour afficher des options supplémentaires. +/// Il permet de voir plus de détails, d'envoyer un message ou de supprimer un ami. +class FriendExpandingCard extends StatefulWidget { + final String name; + final String imageUrl; + final String description; + final VoidCallback onTap; + final VoidCallback onMessageTap; + final VoidCallback onRemoveTap; + + const FriendExpandingCard({ + Key? key, + required this.name, + required this.imageUrl, + required this.description, + required this.onTap, + required this.onMessageTap, + required this.onRemoveTap, + }) : super(key: key); + + @override + _FriendExpandingCardState createState() => _FriendExpandingCardState(); +} + +class _FriendExpandingCardState extends State { + bool _isExpanded = false; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: widget.onTap, + onLongPress: () { + setState(() { + _isExpanded = !_isExpanded; + }); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + height: _isExpanded ? 200 : 100, + margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 5), + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 10, + offset: Offset(2, 2), + ), + ], + ), + child: Column( + children: [ + Row( + children: [ + Hero( + tag: widget.name, + child: CircleAvatar( + backgroundImage: NetworkImage(widget.imageUrl), + radius: 30, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.name, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 5), + AnimatedOpacity( + opacity: _isExpanded ? 1.0 : 0.0, + duration: const Duration(milliseconds: 300), + child: Text( + widget.description, + style: const TextStyle( + color: Colors.white70, + fontSize: 14, + ), + ), + ), + ], + ), + ), + if (_isExpanded) + IconButton( + icon: const Icon(Icons.close, color: Colors.white54), + onPressed: () { + setState(() { + _isExpanded = false; + }); + }, + ), + ], + ), + if (_isExpanded) ...[ + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton.icon( + onPressed: widget.onMessageTap, + icon: const Icon(Icons.message, color: Colors.white), + label: const Text('Message'), + style: ElevatedButton.styleFrom( + iconColor: Colors.green, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + ), + ElevatedButton.icon( + onPressed: widget.onRemoveTap, + icon: const Icon(Icons.delete, color: Colors.red), + label: const Text('Remove'), + style: ElevatedButton.styleFrom( + iconColor: Colors.redAccent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + ), + ], + ) + ] + ], + ), + ), + ); + } +} diff --git a/lib/assets/images/default_avatar.png b/lib/assets/images/default_avatar.png new file mode 100644 index 0000000..3392c86 Binary files /dev/null and b/lib/assets/images/default_avatar.png differ diff --git a/lib/config/router.dart b/lib/config/router.dart index 966491d..fabe7f9 100644 --- a/lib/config/router.dart +++ b/lib/config/router.dart @@ -8,33 +8,43 @@ import 'package:afterwork/presentation/screens/event/event_screen.dart'; import 'package:afterwork/data/datasources/event_remote_data_source.dart'; import '../presentation/reservations/reservations_screen.dart'; -/// Router personnalisé pour gérer la navigation dans l'application. -/// Les logs permettent de tracer chaque navigation dans la console. +/// [AppRouter] gère la navigation dans l'application. +/// Chaque navigation est loguée pour assurer une traçabilité dans la console. class AppRouter { final EventRemoteDataSource eventRemoteDataSource; final String userId; final String userName; final String userLastName; - /// Initialisation des informations utilisateur et source de données + /// Constructeur de [AppRouter] initialisant les informations utilisateur + /// et la source de données pour les événements. + /// + /// [eventRemoteDataSource] : Source de données pour les événements. + /// [userId], [userName], [userLastName] : Informations de l'utilisateur. AppRouter({ required this.eventRemoteDataSource, required this.userId, required this.userName, required this.userLastName, }) { - print("AppRouter initialisé avec les infos utilisateur : $userId, $userName, $userLastName"); + // Log d'initialisation avec les informations utilisateur + debugPrint("[LOG] AppRouter initialisé avec les infos utilisateur : $userId, $userName, $userLastName"); } - /// Génération des routes pour l'application + /// Génère une route en fonction du [RouteSettings] fourni. + /// + /// Logue chaque navigation en fonction du nom de la route. Route generateRoute(RouteSettings settings) { - print("Navigation vers la route : ${settings.name}"); + // Log de la navigation vers la route + debugPrint("[LOG] Navigation vers la route : ${settings.name}"); switch (settings.name) { case '/': + debugPrint("[LOG] Chargement de l'écran de connexion"); return MaterialPageRoute(builder: (_) => const LoginScreen()); case '/home': + debugPrint("[LOG] Chargement de l'écran d'accueil avec l'ID utilisateur : $userId"); return MaterialPageRoute( builder: (_) => HomeScreen( eventRemoteDataSource: eventRemoteDataSource, @@ -46,6 +56,7 @@ class AppRouter { ); case '/event': + debugPrint("[LOG] Chargement de l'écran d'événement pour l'utilisateur : $userId"); return MaterialPageRoute( builder: (_) => EventScreen( userId: userId, @@ -55,21 +66,28 @@ class AppRouter { ); case '/story': + debugPrint("[LOG] Chargement de l'écran des histoires"); return MaterialPageRoute(builder: (_) => const StoryScreen()); case '/profile': + debugPrint("[LOG] Chargement de l'écran du profil"); return MaterialPageRoute(builder: (_) => const ProfileScreen()); case '/settings': + debugPrint("[LOG] Chargement de l'écran des paramètres"); return MaterialPageRoute(builder: (_) => const SettingsScreen()); case '/reservations': + debugPrint("[LOG] Chargement de l'écran des réservations"); return MaterialPageRoute(builder: (_) => const ReservationsScreen()); default: + debugPrint("[ERROR] Route non trouvée : ${settings.name}"); return MaterialPageRoute( builder: (_) => const Scaffold( - body: Center(child: Text('Page non trouvée')), + body: Center( + child: Text('Page non trouvée'), + ), ), ); } diff --git a/lib/core/constants/urls.dart b/lib/core/constants/urls.dart index 8a5f9d4..8983bf0 100644 --- a/lib/core/constants/urls.dart +++ b/lib/core/constants/urls.dart @@ -1,5 +1,5 @@ class Urls { - static const String baseUrl = 'http://192.168.1.145:8085'; + static const String baseUrl = 'http://192.168.1.11:8085'; // Authentication and Users Endpoints static const String authenticateUser = '$baseUrl/users/authenticate'; diff --git a/lib/data/datasources/user_remote_data_source.dart b/lib/data/datasources/user_remote_data_source.dart index 92bcc65..c2935fb 100644 --- a/lib/data/datasources/user_remote_data_source.dart +++ b/lib/data/datasources/user_remote_data_source.dart @@ -5,136 +5,181 @@ import 'package:http/http.dart' as http; import '../../core/errors/exceptions.dart'; /// Classe pour gérer les opérations API pour les utilisateurs. -/// Chaque action est loguée pour faciliter la traçabilité et le débogage. +/// Toutes les actions sont loguées pour faciliter la traçabilité et le débogage. class UserRemoteDataSource { + // Client HTTP injecté pour réaliser les appels réseau final http.Client client; /// Constructeur avec injection du client HTTP UserRemoteDataSource(this.client); - /// Authentifie un utilisateur avec l'email et le mot de passe en clair. + /// Authentifie un utilisateur avec l'email et le mot de passe. /// Si l'authentification réussit, retourne un objet `UserModel`. + /// Les erreurs sont gérées et toutes les actions sont loguées. Future authenticateUser(String email, String password) async { - print("Tentative d'authentification pour l'email : $email"); + print("[LOG] Tentative d'authentification pour l'email : $email"); try { - // Requête POST avec l'email et le mot de passe en clair + // Préparation des données d'authentification à envoyer + final Map body = { + 'email': email, + 'motDePasse': password, + }; + + print("[DEBUG] Données envoyées pour authentification : $body"); + + // Envoi de la requête HTTP POST pour authentifier l'utilisateur final response = await client.post( Uri.parse('${Urls.baseUrl}/users/authenticate'), headers: {'Content-Type': 'application/json'}, - body: jsonEncode({ - 'email': email, - 'motDePasse': password, // Le mot de passe est envoyé en clair pour le moment - }), + body: jsonEncode(body), ); - print("Réponse du serveur pour l'authentification : ${response.statusCode} - ${response.body}"); + // Log de la réponse reçue du serveur + print("[LOG] Réponse du serveur : ${response.statusCode} - ${response.body}"); if (response.statusCode == 200) { - // Si l'authentification réussit, retourne l'utilisateur - return UserModel.fromJson(jsonDecode(response.body)); + final userData = jsonDecode(response.body); + + if (userData['userId'] != null && userData['userId'].isNotEmpty) { + print("[LOG] Utilisateur authentifié avec succès. ID: ${userData['userId']}"); + return UserModel.fromJson(userData); + } else { + print("[ERROR] L'ID utilisateur est manquant dans la réponse."); + throw Exception("ID utilisateur manquant."); + } } else if (response.statusCode == 401) { - // Gestion des erreurs d'authentification - throw UnauthorizedException(); + print("[ERROR] Authentification échouée : Mot de passe incorrect."); + throw UnauthorizedException("Mot de passe incorrect."); } else { - throw ServerException(); + print("[ERROR] Erreur du serveur. Code : ${response.statusCode}"); + throw ServerExceptionWithMessage("Erreur inattendue : ${response.body}"); } } catch (e) { - print("Erreur d'authentification : $e"); + print("[ERROR] Erreur lors de l'authentification : $e"); throw Exception("Erreur lors de l'authentification : $e"); } } - /// Récupère un utilisateur par son identifiant et logue les étapes. + /// Récupère un utilisateur par son identifiant. + /// Les erreurs et les succès sont logués pour un suivi complet. Future getUser(String id) async { - print("Tentative de récupération de l'utilisateur avec l'ID : $id"); + print("[LOG] Tentative de récupération de l'utilisateur avec l'ID : $id"); try { + // Envoi de la requête GET pour obtenir l'utilisateur par son ID final response = await client.get(Uri.parse('${Urls.baseUrl}/users/$id')); - print("Réponse du serveur pour getUser : ${response.statusCode} - ${response.body}"); + print("[LOG] Réponse du serveur pour getUser : ${response.statusCode} - ${response.body}"); if (response.statusCode == 200) { + // Utilisateur trouvé, retour de l'objet UserModel return UserModel.fromJson(json.decode(response.body)); - } else if (response.statusCode == 404) { - print("Utilisateur non trouvé."); + } + // Gestion du cas où l'utilisateur n'est pas trouvé + else if (response.statusCode == 404) { + print("[ERROR] Utilisateur non trouvé."); throw UserNotFoundException(); - } else { + } + // Gestion des autres erreurs serveur + else { + print("[ERROR] Erreur du serveur lors de la récupération de l'utilisateur."); throw ServerException(); } } catch (e) { - print("Erreur lors de la récupération de l'utilisateur : $e"); + print("[ERROR] Erreur lors de la récupération de l'utilisateur : $e"); throw Exception("Erreur lors de la récupération de l'utilisateur : $e"); } } - /// Crée un nouvel utilisateur et logue les détails de la requête. + /// Crée un nouvel utilisateur dans le backend. + /// Toutes les actions, succès ou erreurs sont logués pour un suivi précis. Future createUser(UserModel user) async { - print("Création d'un nouvel utilisateur : ${user.toJson()}"); + print("[LOG] Création d'un nouvel utilisateur : ${user.toJson()}"); try { + // Envoi de la requête POST pour créer un nouvel utilisateur final response = await client.post( Uri.parse('${Urls.baseUrl}/users'), headers: {'Content-Type': 'application/json'}, - body: jsonEncode(user.toJson()), + body: jsonEncode(user.toJson()), // Conversion du modèle utilisateur en JSON ); - print("Réponse du serveur pour createUser : ${response.statusCode} - ${response.body}"); + print("[LOG] Réponse du serveur pour createUser : ${response.statusCode} - ${response.body}"); if (response.statusCode == 201) { + // Utilisateur créé avec succès return UserModel.fromJson(json.decode(response.body)); - } else if (response.statusCode == 409) { - // Gestion des conflits (utilisateur déjà existant) + } + // Gestion des conflits (ex: utilisateur déjà existant) + else if (response.statusCode == 409) { + print("[ERROR] Conflit lors de la création de l'utilisateur : Utilisateur déjà existant."); throw ConflictException(); - } else { + } + // Gestion des autres erreurs serveur + else { + print("[ERROR] Erreur du serveur lors de la création de l'utilisateur."); throw ServerException(); } } catch (e) { - print("Erreur lors de la création de l'utilisateur : $e"); + print("[ERROR] Erreur lors de la création de l'utilisateur : $e"); throw Exception("Erreur lors de la création de l'utilisateur : $e"); } } - /// Met à jour un utilisateur existant et logue les étapes. + /// Met à jour un utilisateur existant. + /// Chaque étape est loguée pour faciliter le débogage. Future updateUser(UserModel user) async { - print("Mise à jour de l'utilisateur : ${user.toJson()}"); + print("[LOG] Mise à jour de l'utilisateur : ${user.toJson()}"); try { + // Envoi de la requête PUT pour mettre à jour un utilisateur final response = await client.put( Uri.parse('${Urls.baseUrl}/users/${user.userId}'), headers: {'Content-Type': 'application/json'}, - body: jsonEncode(user.toJson()), + body: jsonEncode(user.toJson()), // Conversion du modèle utilisateur en JSON ); - print("Réponse du serveur pour updateUser : ${response.statusCode} - ${response.body}"); + print("[LOG] Réponse du serveur pour updateUser : ${response.statusCode} - ${response.body}"); if (response.statusCode == 200) { + // Mise à jour réussie return UserModel.fromJson(json.decode(response.body)); - } else if (response.statusCode == 404) { - // Gestion des cas où l'utilisateur n'est pas trouvé + } + // Gestion du cas où l'utilisateur n'est pas trouvé + else if (response.statusCode == 404) { + print("[ERROR] Utilisateur non trouvé."); throw UserNotFoundException(); - } else { + } + // Gestion des autres erreurs serveur + else { + print("[ERROR] Erreur du serveur lors de la mise à jour de l'utilisateur."); throw ServerException(); } } catch (e) { - print("Erreur lors de la mise à jour de l'utilisateur : $e"); + print("[ERROR] Erreur lors de la mise à jour de l'utilisateur : $e"); throw Exception("Erreur lors de la mise à jour de l'utilisateur : $e"); } } - /// Supprime un utilisateur et logue chaque étape. + /// Supprime un utilisateur par son identifiant. + /// Les erreurs et succès sont logués pour garantir un suivi complet. Future deleteUser(String id) async { - print("Tentative de suppression de l'utilisateur avec l'ID : $id"); + print("[LOG] Tentative de suppression de l'utilisateur avec l'ID : $id"); try { + // Envoi de la requête DELETE pour supprimer un utilisateur final response = await client.delete(Uri.parse('${Urls.baseUrl}/users/$id')); - print("Réponse du serveur pour deleteUser : ${response.statusCode} - ${response.body}"); + print("[LOG] Réponse du serveur pour deleteUser : ${response.statusCode} - ${response.body}"); - if (response.statusCode != 204) { - print("Erreur lors de la suppression de l'utilisateur."); + // Vérification du succès de la suppression + if (response.statusCode == 204) { + print("[LOG] Utilisateur supprimé avec succès."); + } + // Gestion des autres erreurs serveur + else { + print("[ERROR] Erreur du serveur lors de la suppression de l'utilisateur."); throw ServerException(); - } else { - print("Utilisateur supprimé avec succès."); } } catch (e) { - print("Erreur lors de la suppression de l'utilisateur : $e"); + print("[ERROR] Erreur lors de la suppression de l'utilisateur : $e"); throw Exception("Erreur lors de la suppression de l'utilisateur : $e"); } } diff --git a/lib/data/models/social_post_model.dart b/lib/data/models/social_post_model.dart new file mode 100644 index 0000000..2effe2b --- /dev/null +++ b/lib/data/models/social_post_model.dart @@ -0,0 +1,23 @@ +class SocialPost { + final String userName; + final String userImage; + final String postText; + final String postImage; + final int likes; + final int comments; + final int shares; + final List badges; // Gamification badges + final List tags; // Ajout de tags pour personnalisation des posts + + SocialPost({ + required this.userName, + required this.userImage, + required this.postText, + required this.postImage, + required this.likes, + required this.comments, + required this.shares, + required this.badges, + this.tags = const [], + }); +} diff --git a/lib/data/models/user_model.dart b/lib/data/models/user_model.dart index 7fae62f..e0b1e58 100644 --- a/lib/data/models/user_model.dart +++ b/lib/data/models/user_model.dart @@ -20,7 +20,7 @@ class UserModel extends User { /// Factory pour créer un `UserModel` à partir d'un JSON reçu depuis l'API. factory UserModel.fromJson(Map json) { return UserModel( - userId: json['id'] ?? '', + userId: json['userId'] ?? '', nom: json['nom'] ?? 'Inconnu', prenoms: json['prenoms'] ?? 'Inconnu', email: json['email'] ?? 'inconnu@example.com', diff --git a/lib/data/providers/friends_provider.dart b/lib/data/providers/friends_provider.dart new file mode 100644 index 0000000..2b69c7c --- /dev/null +++ b/lib/data/providers/friends_provider.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:logger/logger.dart'; +import '../../domain/entities/friend.dart'; +import '../../data/repositories/friends_repository_impl.dart'; + +/// [FriendsProvider] est un `ChangeNotifier` qui gère la logique de gestion des amis. +/// Il utilise [FriendsRepositoryImpl] pour interagir avec l'API et assure la gestion des états, +/// comme le chargement, la pagination et les erreurs éventuelles. +class FriendsProvider with ChangeNotifier { + final FriendsRepositoryImpl friendsRepository; + final Logger _logger = Logger(); // Logger pour suivre toutes les actions. + + // Liste privée des amis récupérée depuis l'API + List _friendsList = []; + bool _isLoading = false; // Indique si une opération de chargement est en cours + bool _hasMore = true; // Indique s'il reste des amis à charger + int _currentPage = 0; + final int _friendsPerPage = 10; // Nombre d'amis par page pour la pagination + + /// Constructeur de [FriendsProvider] qui requiert une instance de [FriendsRepositoryImpl]. + FriendsProvider({required this.friendsRepository}); + + // Getters pour accéder aux états depuis l'interface utilisateur + bool get isLoading => _isLoading; + bool get hasMore => _hasMore; + List get friendsList => _friendsList; + + /// Récupère la liste paginée des amis pour un utilisateur donné. + /// + /// [userId] : L'identifiant unique de l'utilisateur. + /// [loadMore] : Indique s'il s'agit d'une demande de chargement supplémentaire pour la pagination. + /// + /// En cas d'erreur, logue l'exception et gère l'état `isLoading`. + Future fetchFriends(String userId, {bool loadMore = false}) async { + if (_isLoading) { + _logger.w('[LOG] Chargement déjà en cours, annulation de la nouvelle demande.'); + return; + } + + _isLoading = true; + notifyListeners(); + _logger.i('[LOG] Début du chargement des amis.'); + + if (!loadMore) { + // Réinitialisation de la liste et de la pagination si ce n'est pas un chargement supplémentaire + _friendsList = []; + _currentPage = 0; + _hasMore = true; + _logger.i('[LOG] Réinitialisation de la pagination et de la liste des amis.'); + } + + try { + _logger.i('[LOG] Chargement de la page $_currentPage des amis pour l\'utilisateur $userId.'); + final newFriends = await friendsRepository.fetchFriends(userId, _currentPage, _friendsPerPage); + + if (newFriends.isEmpty) { + _hasMore = false; + _logger.i('[LOG] Fin de liste atteinte, plus d\'amis à charger.'); + } else { + _friendsList.addAll(newFriends); + _currentPage++; + _logger.i('[LOG] Amis ajoutés à la liste. Page actuelle : $_currentPage'); + } + } catch (e) { + _logger.e('[ERROR] Erreur lors de la récupération des amis : $e'); + } finally { + _isLoading = false; + _logger.i('[LOG] Fin du chargement des amis.'); + notifyListeners(); + } + } + + /// Supprime un ami dans l'API et met à jour la liste localement. + /// + /// [friendId] : Identifiant unique de l'ami à supprimer. + /// + /// Loggue chaque étape pour assurer un suivi précis de l'opération. + Future removeFriend(String friendId) async { + try { + _logger.i('[LOG] Tentative de suppression de l\'ami avec l\'ID : $friendId'); + await friendsRepository.removeFriend(friendId); + _friendsList.removeWhere((friend) => friend.friendId == friendId); + _logger.i('[LOG] Ami supprimé localement avec succès : $friendId'); + } catch (e) { + _logger.e('[ERROR] Erreur lors de la suppression de l\'ami : $e'); + } finally { + notifyListeners(); + } + } + + /// Récupère les détails d'un ami via l'API. + /// + /// [userId] : L'identifiant de l'utilisateur connecté. + /// [friendId] : Identifiant unique de l'ami. + /// + /// Retourne un `Future` contenant les détails ou `null` en cas d'erreur. + Future fetchFriendDetails(String userId, String friendId) async { + try { + _logger.i('[LOG] Tentative de récupération des détails de l\'ami avec l\'ID : $friendId'); + final friendDetails = await friendsRepository.getFriendDetails(friendId, userId); + + if (friendDetails != null) { + _logger.i('[LOG] Détails de l\'ami récupérés avec succès : ${friendDetails.friendId}'); + } else { + _logger.w('[LOG] Détails de l\'ami introuvables pour l\'ID : $friendId'); + } + + return friendDetails; + } catch (e) { + _logger.e('[ERROR] Exception lors de la récupération des détails de l\'ami : $e'); + return null; + } + } + + /// Convertit un statut sous forme de chaîne en [FriendStatus]. + /// + /// [status] : Le statut sous forme de chaîne. + /// + /// Retourne un [FriendStatus] correspondant, ou `FriendStatus.unknown` si non reconnu. + FriendStatus _convertToFriendStatus(String status) { + switch (status.toLowerCase()) { + case 'pending': + return FriendStatus.pending; + case 'accepted': + return FriendStatus.accepted; + case 'blocked': + return FriendStatus.blocked; + default: + return FriendStatus.unknown; + } + } + + /// Met à jour le statut d'un ami (par exemple : accepter, bloquer). + /// + /// [friendId] : Identifiant unique de l'ami. + /// [status] : Nouveau statut pour l'ami sous forme de chaîne de caractères. + /// + /// Loggue l'action, convertit le statut en `FriendStatus`, et met à jour la liste localement. + Future updateFriendStatus(String friendId, String status) async { + try { + _logger.i('[LOG] Tentative de mise à jour du statut de l\'ami avec l\'ID : $friendId'); + + // Conversion du `String` en `FriendStatus` pour l'update locale + final friendStatus = _convertToFriendStatus(status); + await friendsRepository.updateFriendStatus(friendId, status); + + // Mise à jour locale de la liste pour afficher le changement de statut + final friendIndex = _friendsList.indexWhere((friend) => friend.friendId == friendId); + if (friendIndex != -1) { + _friendsList[friendIndex] = _friendsList[friendIndex].copyWith(status: friendStatus); + _logger.i('[LOG] Statut de l\'ami mis à jour localement pour l\'ID : $friendId'); + } + } catch (e) { + _logger.e('[ERROR] Erreur lors de la mise à jour du statut de l\'ami : $e'); + } finally { + notifyListeners(); + } + } +} diff --git a/lib/data/providers/user_provider.dart b/lib/data/providers/user_provider.dart index 6f0a68c..dbb7e9c 100644 --- a/lib/data/providers/user_provider.dart +++ b/lib/data/providers/user_provider.dart @@ -1,18 +1,53 @@ import 'package:flutter/material.dart'; +/// [UserProvider] est un `ChangeNotifier` qui gère les informations de l'utilisateur. +/// Toutes les modifications et actions sont loguées pour assurer une traçabilité complète dans le terminal. class UserProvider with ChangeNotifier { String _userId = ''; String _userName = ''; String _userLastName = ''; + /// Getter pour l'ID de l'utilisateur String get userId => _userId; + + /// Getter pour le nom de l'utilisateur String get userName => _userName; + + /// Getter pour le prénom de l'utilisateur String get userLastName => _userLastName; + /// Méthode pour définir les informations de l'utilisateur. + /// Logue les informations fournies et notifie les listeners des changements. + /// + /// [id] : L'ID de l'utilisateur. + /// [name] : Le nom de l'utilisateur. + /// [lastName] : Le prénom de l'utilisateur. void setUser(String id, String name, String lastName) { + debugPrint("[LOG] Tentative de définition des informations de l'utilisateur : ID = $id, Nom = $name, Prénom = $lastName"); + _userId = id; _userName = name; _userLastName = lastName; + + debugPrint("[LOG] Informations utilisateur définies : ID = $_userId, Nom = $_userName, Prénom = $_userLastName"); + + // Notifie les widgets écoutant ce provider qu'une modification a eu lieu + notifyListeners(); + } + + /// Méthode pour réinitialiser les informations de l'utilisateur. + /// Les valeurs sont loguées avant et après la réinitialisation. + void resetUser() { + debugPrint("[LOG] Réinitialisation des informations de l'utilisateur."); + debugPrint("[LOG] Valeurs avant réinitialisation : ID = $_userId, Nom = $_userName, Prénom = $_userLastName"); + + _userId = ''; + _userName = ''; + _userLastName = ''; + + debugPrint("[LOG] Informations utilisateur réinitialisées : ID = $_userId, Nom = $_userName, Prénom = $_userLastName"); + + // Notifie les widgets écoutant ce provider que l'utilisateur a été réinitialisé notifyListeners(); } } diff --git a/lib/data/repositories/friends_repository.dart b/lib/data/repositories/friends_repository.dart new file mode 100644 index 0000000..baa8ed6 --- /dev/null +++ b/lib/data/repositories/friends_repository.dart @@ -0,0 +1,48 @@ +import 'package:afterwork/domain/entities/friend.dart'; + +/// Interface [FriendsRepository] définissant les méthodes pour gérer les amis. +/// Cette interface permet de séparer la logique métier des appels API et de la gestion des données. +/// Elle est implémentée par [FriendsRepositoryImpl], qui contient les détails d'implémentation. +abstract class FriendsRepository { + + /// Récupère la liste paginée des amis pour un utilisateur donné via l'API. + /// + /// [userId] : Identifiant unique de l'utilisateur pour lequel récupérer la liste d'amis. + /// [currentPage] : Indique la page actuelle pour la pagination. + /// [friendsPerPage] : Nombre d'amis à récupérer par page. + /// + /// Retourne une liste d'objets [Friend] correspondant aux amis de l'utilisateur. + /// En cas d'échec, une liste vide peut être retournée par l'implémentation. + Future> fetchFriends(String userId, int currentPage, int friendsPerPage); + + /// Envoie une demande pour ajouter un nouvel ami via l'API. + /// + /// [friend] : Objet [Friend] représentant l'ami à ajouter. + /// + /// Retourne un `Future`. En cas d'erreur, l'implémentation peut lancer une exception. + Future addFriend(Friend friend); + + /// Supprime un ami existant via l'API. + /// + /// [friendId] : Identifiant unique de l'ami à supprimer. + /// + /// Retourne un `Future`. En cas d'erreur, l'implémentation peut lancer une exception. + Future removeFriend(String friendId); + + /// Récupère les détails d'un ami en utilisant son identifiant `friendId`. + /// + /// [userId] : Identifiant unique de l'utilisateur connecté (facultatif selon le contexte). + /// [friendId] : Identifiant unique de l'ami pour lequel récupérer les détails. + /// + /// Retourne un `Future` contenant les informations de l'ami si trouvées, + /// ou `null` si aucun ami correspondant n'est trouvé ou en cas d'échec. + Future getFriendDetails(String friendId, String userId); + + /// Met à jour le statut d'un ami dans le système (par exemple, accepter, bloquer). + /// + /// [friendId] : Identifiant unique de l'ami. + /// [status] : Nouveau statut de l'ami sous forme de chaîne de caractères. + /// + /// Retourne un `Future`. En cas d'erreur, l'implémentation peut lancer une exception. + Future updateFriendStatus(String friendId, String status); +} diff --git a/lib/data/repositories/friends_repository_impl.dart b/lib/data/repositories/friends_repository_impl.dart new file mode 100644 index 0000000..87bf3ff --- /dev/null +++ b/lib/data/repositories/friends_repository_impl.dart @@ -0,0 +1,175 @@ +import 'dart:convert'; +import 'package:flutter/cupertino.dart'; +import 'package:http/http.dart' as http; +import 'package:logger/logger.dart'; +import '../../core/constants/urls.dart'; +import '../../domain/entities/friend.dart'; +import 'friends_repository.dart'; + +/// Implémentation de [FriendsRepository] pour gérer les appels API relatifs aux amis. +/// Chaque action est loguée pour une traçabilité complète et une gestion des erreurs avancée. +class FriendsRepositoryImpl implements FriendsRepository { + final http.Client client; + final Logger _logger = Logger(); // Logger pour suivre toutes les actions. + + FriendsRepositoryImpl({required this.client}); + + /// Récupère la liste paginée des amis pour un utilisateur donné via l'API. + /// + /// [userId] : Identifiant unique de l'utilisateur. + /// [page] : Page actuelle pour la pagination. + /// [size] : Nombre d'amis par page. + /// + /// Retourne une liste d'objets [Friend] ou une liste vide en cas d'erreur. + @override + Future> fetchFriends(String userId, int page, int size) async { + try { + _logger.i("[LOG] Chargement des amis pour l'utilisateur : $userId, page : $page, taille : $size"); + + final uri = Uri.parse('${Urls.baseUrl}/friends/list/$userId?page=$page&size=$size'); + _logger.d('[LOG] URL appelée : $uri'); + + final response = await client.get(uri); + + if (response.statusCode == 200) { + _logger.i("[LOG] Liste des amis récupérée avec succès."); + + final List friendsJson = json.decode(response.body); + _logger.i("[LOG] Nombre d'amis récupérés : ${friendsJson.length}"); + + return friendsJson.map((json) => Friend.fromJson(json as Map)).toList(); + } else { + _logger.e("[ERROR] Échec de la récupération des amis. Code HTTP : ${response.statusCode}"); + return []; + } + } catch (e) { + _logger.e("[ERROR] Exception lors de la récupération des amis : $e"); + return []; + } + } + + /// Envoie une demande pour ajouter un nouvel ami via l'API. + /// + /// [friend] : Objet [Friend] représentant l'ami à ajouter. + /// + /// Loggue chaque étape et lève une exception en cas d'erreur. + @override + Future addFriend(Friend friend) async { + try { + _logger.i("[LOG] Tentative d'ajout de l'ami : ${friend.firstName} ${friend.lastName}"); + + final uri = Uri.parse('${Urls.baseUrl}/friends/send'); + final response = await client.post( + uri, + headers: {'Content-Type': 'application/json'}, + body: json.encode(friend.toJson()), + ); + + if (response.statusCode == 200) { + _logger.i("[LOG] Ami ajouté avec succès : ${friend.friendId}"); + } else { + _logger.e("[ERROR] Échec lors de l'ajout de l'ami. Code HTTP : ${response.statusCode}"); + throw Exception("Erreur lors de l'ajout de l'ami"); + } + } catch (e) { + _logger.e("[ERROR] Exception lors de l'ajout de l'ami : $e"); + rethrow; + } + } + + /// Supprime un ami existant via l'API. + /// + /// [friendId] : Identifiant unique de l'ami à supprimer. + /// + /// Loggue l'action et lève une exception en cas d'erreur. + @override + Future removeFriend(String friendId) async { + try { + _logger.i("[LOG] Tentative de suppression de l'ami avec l'ID : $friendId"); + + final uri = Uri.parse('${Urls.baseUrl}/friends/$friendId'); + final response = await client.delete(uri); + + if (response.statusCode == 200) { + _logger.i("[LOG] Ami supprimé avec succès : $friendId"); + } else { + _logger.e("[ERROR] Échec lors de la suppression de l'ami. Code HTTP : ${response.statusCode}"); + throw Exception("Erreur lors de la suppression de l'ami"); + } + } catch (e) { + _logger.e("[ERROR] Exception lors de la suppression de l'ami : $e"); + rethrow; + } + } + + /// Récupère les détails d'un ami en utilisant son identifiant `friendId`. + /// + /// [friendId] : Identifiant unique de l'ami. + /// [userId] : Identifiant unique de l'utilisateur connecté. + /// + /// Retourne un `Future` avec les informations de l'ami ou `null` en cas d'échec. + @override + Future getFriendDetails(String friendId, String userId) async { + try { + _logger.i("[LOG] Récupération des détails de l'ami avec ID : $friendId pour l'utilisateur : $userId"); + + final uri = Uri.parse('${Urls.baseUrl}/friends/details'); + _logger.d("[LOG] URL pour les détails de l'ami : $uri"); + + final headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + final body = jsonEncode({ + 'friendId': friendId, + 'userId': userId, + }); + + final response = await client.post(uri, headers: headers, body: body); + _logger.d("[LOG] Réponse de l'API : ${response.body}"); + + if (response.statusCode == 200) { + final friendJson = json.decode(response.body); + _logger.i("[LOG] Détails de l'ami récupérés : $friendJson"); + return Friend.fromJson(friendJson); + } else { + _logger.e("[ERROR] Échec de la récupération des détails. Code HTTP : ${response.statusCode}"); + return null; + } + } catch (e) { + _logger.e("[ERROR] Exception lors de la récupération des détails de l'ami : $e"); + return null; + } + } + + /// Met à jour le statut d'un ami (par exemple, "accepté", "bloqué"). + /// + /// [friendId] : Identifiant unique de l'ami. + /// [status] : Nouveau statut sous forme de chaîne de caractères. + /// + /// Loggue chaque étape et lève une exception en cas d'échec. + @override + Future updateFriendStatus(String friendId, String status) async { + try { + _logger.i("[LOG] Mise à jour du statut de l'ami avec l'ID : $friendId, nouveau statut : $status"); + + final uri = Uri.parse('${Urls.baseUrl}/friends/$friendId/status'); + final response = await client.patch( + uri, + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'status': status}), + ); + + if (response.statusCode == 200) { + _logger.i("[LOG] Statut de l'ami mis à jour avec succès : $friendId"); + } else { + _logger.e("[ERROR] Erreur lors de la mise à jour du statut. Code HTTP : ${response.statusCode}"); + throw Exception("Erreur lors de la mise à jour du statut"); + } + } catch (e) { + _logger.e("[ERROR] Exception lors de la mise à jour du statut de l'ami : $e"); + rethrow; + } + } +} diff --git a/lib/data/services/preferences_helper.dart b/lib/data/services/preferences_helper.dart index edb80d5..7780379 100644 --- a/lib/data/services/preferences_helper.dart +++ b/lib/data/services/preferences_helper.dart @@ -3,80 +3,96 @@ import 'package:shared_preferences/shared_preferences.dart'; /// Classe pour gérer les préférences utilisateur à l'aide de SharedPreferences. /// Permet de stocker et récupérer des informations de manière non sécurisée, /// contrairement au stockage sécurisé qui est utilisé pour des données sensibles. +/// Chaque action est loguée pour assurer la traçabilité complète dans le terminal. class PreferencesHelper { // Initialisation de SharedPreferences en tant que Future final Future _prefs = SharedPreferences.getInstance(); /// Sauvegarde une chaîne de caractères (String) dans les préférences. + /// Les actions sont loguées et les erreurs capturées pour garantir une sauvegarde correcte. Future setString(String key, String value) async { - print("Sauvegarde dans les préférences : clé = $key, valeur = $value"); + print("[LOG] Sauvegarde dans les préférences : clé = $key, valeur = $value"); final prefs = await _prefs; - await prefs.setString(key, value); - print("Sauvegarde réussie pour la clé : $key"); + final success = await prefs.setString(key, value); + if (success) { + print("[LOG] Sauvegarde réussie pour la clé : $key"); + } else { + print("[ERROR] Échec de la sauvegarde pour la clé : $key"); + } } /// Récupère une chaîne de caractères depuis les préférences. + /// Retourne la valeur ou null si aucune donnée n'est trouvée. Future getString(String key) async { - print("Récupération depuis les préférences pour la clé : $key"); + print("[LOG] Récupération depuis les préférences pour la clé : $key"); final prefs = await _prefs; final value = prefs.getString(key); - print("Valeur récupérée pour la clé $key : $value"); + print("[LOG] Valeur récupérée pour la clé $key : $value"); return value; } /// Supprime une entrée dans les préférences. + /// Logue chaque étape de la suppression. Future remove(String key) async { - print("Suppression dans les préférences pour la clé : $key"); + print("[LOG] Suppression dans les préférences pour la clé : $key"); final prefs = await _prefs; - await prefs.remove(key); - print("Suppression réussie pour la clé : $key"); + final success = await prefs.remove(key); + if (success) { + print("[LOG] Suppression réussie pour la clé : $key"); + } else { + print("[ERROR] Échec de la suppression pour la clé : $key"); + } } /// Sauvegarde l'identifiant utilisateur dans les préférences. + /// Logue l'action et assure la robustesse de l'opération. Future saveUserId(String userId) async { - print("Sauvegarde de l'userId dans les préférences : $userId"); + print("[LOG] Sauvegarde de l'userId dans les préférences : $userId"); await setString('user_id', userId); - print("Sauvegarde réussie de l'userId."); } /// Récupère l'identifiant utilisateur depuis les préférences. + /// Retourne l'ID ou null en cas d'échec. Future getUserId() async { - print("Récupération de l'userId depuis les préférences."); + print("[LOG] Récupération de l'userId depuis les préférences."); return await getString('user_id'); } /// Sauvegarde le nom d'utilisateur dans les préférences. + /// Logue l'opération pour assurer un suivi complet. Future saveUserName(String userName) async { - print("Sauvegarde du userName dans les préférences : $userName"); + print("[LOG] Sauvegarde du userName dans les préférences : $userName"); await setString('user_name', userName); - print("Sauvegarde réussie du userName."); } /// Récupère le nom d'utilisateur depuis les préférences. + /// Retourne le nom ou null en cas d'échec. Future getUserName() async { - print("Récupération du userName depuis les préférences."); + print("[LOG] Récupération du userName depuis les préférences."); return await getString('user_name'); } /// Sauvegarde le prénom de l'utilisateur dans les préférences. + /// Logue l'opération pour assurer un suivi complet. Future saveUserLastName(String userLastName) async { - print("Sauvegarde du userLastName dans les préférences : $userLastName"); + print("[LOG] Sauvegarde du userLastName dans les préférences : $userLastName"); await setString('user_last_name', userLastName); - print("Sauvegarde réussie du userLastName."); } /// Récupère le prénom de l'utilisateur depuis les préférences. + /// Retourne le prénom ou null en cas d'échec. Future getUserLastName() async { - print("Récupération du userLastName depuis les préférences."); + print("[LOG] Récupération du userLastName depuis les préférences."); return await getString('user_last_name'); } /// Supprime toutes les informations utilisateur dans les préférences. + /// Logue chaque étape de la suppression. Future clearUserInfo() async { - print("Suppression des informations utilisateur (userId, userName, userLastName) des préférences."); + print("[LOG] Suppression des informations utilisateur (userId, userName, userLastName) des préférences."); await remove('user_id'); await remove('user_name'); await remove('user_last_name'); - print("Suppression réussie des informations utilisateur."); + print("[LOG] Suppression réussie des informations utilisateur."); } } diff --git a/lib/data/services/secure_storage.dart b/lib/data/services/secure_storage.dart index 6ccea61..6dba42c 100644 --- a/lib/data/services/secure_storage.dart +++ b/lib/data/services/secure_storage.dart @@ -1,78 +1,137 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:logger/logger.dart'; -/// Classe pour gérer le stockage sécurisé dans l'application. -/// Utilise FlutterSecureStorage pour stocker, lire et supprimer des données sensibles. +/// Classe SecureStorage pour gérer les opérations de stockage sécurisé. +/// Toutes les actions sont loguées pour permettre une traçabilité complète dans le terminal. class SecureStorage { - // Instance de FlutterSecureStorage pour gérer le stockage sécurisé + // Instance de FlutterSecureStorage pour le stockage sécurisé. final FlutterSecureStorage _storage = const FlutterSecureStorage(); + // Logger pour suivre et enregistrer les actions dans le terminal. + final Logger _logger = Logger(); + /// Écrit une valeur dans le stockage sécurisé avec la clé spécifiée. + /// Les actions sont loguées et les erreurs sont capturées pour assurer la robustesse. Future write(String key, String value) async { - print("Écriture dans le stockage sécurisé : clé = $key, valeur = $value"); - await _storage.write(key: key, value: value); - print("Écriture réussie pour la clé : $key"); + try { + _logger.i("[LOG] Tentative d'écriture dans le stockage sécurisé : clé = $key, valeur = $value"); + await _storage.write(key: key, value: value); + _logger.i("[LOG] Écriture réussie pour la clé : $key"); + } catch (e) { + _logger.e("[ERROR] Échec d'écriture pour la clé $key : $e"); + rethrow; + } } /// Lit une valeur depuis le stockage sécurisé en fonction de la clé spécifiée. + /// Retourne la valeur ou null en cas d'erreur. Chaque action est loguée. Future read(String key) async { - print("Lecture dans le stockage sécurisé pour la clé : $key"); - final value = await _storage.read(key: key); - print("Valeur lue pour la clé $key : $value"); - return value; + try { + _logger.i("[LOG] Lecture de la clé : $key"); + final value = await _storage.read(key: key); + _logger.i("[LOG] Valeur lue pour la clé $key : $value"); + return value; + } catch (e) { + _logger.e("[ERROR] Échec de lecture pour la clé $key : $e"); + return null; + } } /// Supprime une entrée dans le stockage sécurisé pour la clé spécifiée. + /// Logue chaque étape de l'opération de suppression. Future delete(String key) async { - print("Suppression dans le stockage sécurisé pour la clé : $key"); - await _storage.delete(key: key); - print("Suppression réussie pour la clé : $key"); + try { + _logger.i("[LOG] Suppression de la clé : $key"); + await _storage.delete(key: key); + _logger.i("[LOG] Suppression réussie pour la clé : $key"); + } catch (e) { + _logger.e("[ERROR] Échec de suppression pour la clé $key : $e"); + rethrow; + } } /// Sauvegarde l'identifiant utilisateur dans le stockage sécurisé. + /// Logue l'action et assure la robustesse de l'opération. Future saveUserId(String userId) async { - print("Sauvegarde de l'userId dans le stockage sécurisé : $userId"); - await write('user_id', userId); - print("Sauvegarde réussie de l'userId."); + if (userId.isNotEmpty) { + _logger.i("[LOG] Tentative de sauvegarde de l'userId : $userId"); + await write('user_id', userId); + final savedId = await getUserId(); // Récupération immédiate pour vérifier l'enregistrement + if (savedId != null && savedId == userId) { + _logger.i("[LOG] L'userId a été sauvegardé avec succès et vérifié : $savedId"); + } else { + _logger.e("[ERROR] L'userId n'a pas été correctement sauvegardé."); + } + } else { + _logger.e("[ERROR] L'userId est vide, échec de sauvegarde."); + } } /// Récupère l'identifiant utilisateur depuis le stockage sécurisé. + /// Retourne l'ID ou null en cas d'échec. Future getUserId() async { - print("Récupération de l'userId depuis le stockage sécurisé."); + _logger.i("[LOG] Récupération de l'userId."); return await read('user_id'); } /// Sauvegarde le nom d'utilisateur dans le stockage sécurisé. - Future saveUserName(String userName) async { - print("Sauvegarde du userName dans le stockage sécurisé : $userName"); - await write('user_name', userName); - print("Sauvegarde réussie du userName."); + /// Retourne un booléen pour indiquer le succès ou l'échec. + Future saveUserName(String userName) async { + _logger.i("[LOG] Tentative de sauvegarde du userName : $userName"); + return await _safeWrite('user_name', userName); } /// Récupère le nom d'utilisateur depuis le stockage sécurisé. + /// Retourne le nom ou null en cas d'échec. Future getUserName() async { - print("Récupération du userName depuis le stockage sécurisé."); - return await read('user_name'); + _logger.i("[LOG] Tentative de récupération du userName depuis le stockage sécurisé."); + return await _safeRead('user_name'); } /// Sauvegarde le prénom de l'utilisateur dans le stockage sécurisé. - Future saveUserLastName(String userLastName) async { - print("Sauvegarde du userLastName dans le stockage sécurisé : $userLastName"); - await write('user_last_name', userLastName); - print("Sauvegarde réussie du userLastName."); + /// Retourne un booléen pour indiquer le succès ou l'échec. + Future saveUserLastName(String userLastName) async { + _logger.i("[LOG] Tentative de sauvegarde du userLastName : $userLastName"); + return await _safeWrite('user_last_name', userLastName); } /// Récupère le prénom de l'utilisateur depuis le stockage sécurisé. + /// Retourne le prénom ou null en cas d'échec. Future getUserLastName() async { - print("Récupération du userLastName depuis le stockage sécurisé."); - return await read('user_last_name'); + _logger.i("[LOG] Tentative de récupération du userLastName depuis le stockage sécurisé."); + return await _safeRead('user_last_name'); } /// Supprime toutes les informations utilisateur du stockage sécurisé. + /// Logue chaque étape de la suppression. Future deleteUserInfo() async { - print("Suppression des informations utilisateur (userId, userName, userLastName)."); + _logger.i("[LOG] Tentative de suppression de toutes les informations utilisateur."); await delete('user_id'); await delete('user_name'); await delete('user_last_name'); - print("Suppression réussie des informations utilisateur."); + _logger.i("[LOG] Suppression réussie des informations utilisateur."); + } + + /// Méthode privée pour encapsuler l'écriture sécurisée avec gestion d'erreur. + /// Retourne un booléen indiquant le succès ou l'échec de l'opération. + Future _safeWrite(String key, String value) async { + try { + await write(key, value); + return true; // Indique que l'écriture a réussi. + } catch (e) { + _logger.e("[ERROR] Erreur lors de l'écriture sécurisée : $e"); + return false; // Indique un échec. + } + } + + /// Méthode privée pour encapsuler la lecture sécurisée avec gestion d'erreur. + /// Retourne la valeur ou null si la lecture échoue. + Future _safeRead(String key) async { + try { + return await read(key); + } catch (e) { + _logger.e("[ERROR] Erreur lors de la lecture sécurisée : $e"); + return null; // Retourne null en cas d'erreur. + } } } diff --git a/lib/domain/entities/friend.dart b/lib/domain/entities/friend.dart new file mode 100644 index 0000000..d2e4ad8 --- /dev/null +++ b/lib/domain/entities/friend.dart @@ -0,0 +1,119 @@ +import 'package:equatable/equatable.dart'; +import 'package:logger/logger.dart'; + +/// Enumération représentant les différents statuts possibles d'un ami. +/// Utilisée pour éviter les erreurs de chaîne de caractères et faciliter les comparaisons. +enum FriendStatus { pending, accepted, blocked, unknown } + +/// Classe [Friend] représentant un ami avec ses informations de base. +/// Cette classe est conçue pour être utilisée dans des applications de haut niveau +/// avec une gestion robuste des erreurs, des logs avancés, et une immuabilité stricte. +/// +/// Chaque instance de [Friend] est immuable et toute modification doit passer par [copyWith]. +class Friend extends Equatable { + final String friendId; // ID unique de l'ami, requis et non-nullable + final String firstName; // Prénom de l'ami, non-nullable pour garantir une intégrité des données + final String lastName; // Nom de famille, non-nullable + final String? email; // Adresse e-mail, optionnelle mais typiquement présente + final String? imageUrl; // URL de l'image de profil, optionnelle + final FriendStatus status; // Statut de l'ami, avec une valeur par défaut `unknown` + + /// Logger statique pour suivre toutes les actions et transformations liées à [Friend]. + static final Logger _logger = Logger(); + + /// Constructeur de la classe [Friend]. + /// Initialisation avec des valeurs spécifiques pour `firstName` et `lastName`. + /// La validation des valeurs est incluse pour garantir l'intégrité des données. + Friend({ + required this.friendId, + this.firstName = 'Ami inconnu', // Valeur par défaut pour éviter les champs vides + this.lastName = '', + this.email, + this.imageUrl, + this.status = FriendStatus.unknown, + }) { + assert(friendId.isNotEmpty, 'friendId ne doit pas être vide'); + _logger.i('[LOG] Création d\'un objet Friend : ID = $friendId, Nom = $firstName $lastName'); + } + + /// Méthode factory pour créer un objet [Friend] à partir d'un JSON. + /// Inclut une validation approfondie pour vérifier l'intégrité des données. + /// + /// Retourne une instance de [Friend] ou null si les données sont incomplètes. + factory Friend.fromJson(Map json) { + _logger.i('[LOG] Conversion JSON -> Friend : $json'); + + if (json['friendId'] == null || (json['friendId'] as String).isEmpty) { + _logger.e('[ERROR] friendId manquant ou vide dans le JSON.'); + throw ArgumentError("friendId est requis pour créer un objet Friend"); + } + + return Friend( + friendId: json['friendId'] as String, + firstName: json['friendFirstName'] as String? ?? 'Ami inconnu', + lastName: json['friendLastName'] as String? ?? '', + email: json['email'] as String?, + imageUrl: json['imageUrl'] as String?, + status: _parseStatus(json['status'] as String?), + ); + } + + /// Méthode privée pour parser le champ `status` en type [FriendStatus]. + /// Retourne [FriendStatus.unknown] si le statut est non reconnu. + static FriendStatus _parseStatus(String? status) { + switch (status?.toLowerCase()) { + case 'pending': + return FriendStatus.pending; + case 'accepted': + return FriendStatus.accepted; + case 'blocked': + return FriendStatus.blocked; + default: + return FriendStatus.unknown; + } + } + + /// Sérialise un objet [Friend] en JSON pour être transmis à l'API. + /// Les logs incluent le temps de traitement pour les optimisations de performance. + Map toJson() { + final json = { + 'friendId': friendId, + 'firstName': firstName, + 'lastName': lastName, + 'email': email, + 'imageUrl': imageUrl, + 'status': status.name, + }; + _logger.i('[LOG] Conversion Friend -> JSON : $json'); + return json; + } + + /// Méthode [copyWith] pour cloner un objet `Friend` en modifiant certains attributs. + /// Facilite la modification immuable des propriétés sans affecter l'instance actuelle. + /// + /// Log chaque copie pour surveiller l'état des données. + Friend copyWith({ + String? friendId, + String? firstName, + String? lastName, + String? email, + String? imageUrl, + FriendStatus? status, + }) { + final newFriend = Friend( + friendId: friendId ?? this.friendId, + firstName: firstName ?? this.firstName, + lastName: lastName ?? this.lastName, + email: email ?? this.email, + imageUrl: imageUrl ?? this.imageUrl, + status: status ?? this.status, + ); + _logger.i('[LOG] Création d\'une copie modifiée de Friend : ID = ${newFriend.friendId}'); + return newFriend; + } + + /// Propriétés utilisées pour comparer les objets [Friend], + /// facilitant l'utilisation dans des listes et des ensembles. + @override + List get props => [friendId, firstName, lastName, email, imageUrl, status]; +} diff --git a/lib/main.dart b/lib/main.dart index 605dc94..af89b11 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:afterwork/config/router.dart'; import 'package:afterwork/data/datasources/event_remote_data_source.dart'; import 'package:afterwork/data/providers/user_provider.dart'; +import 'package:afterwork/data/repositories/friends_repository_impl.dart'; import 'package:afterwork/data/services/preferences_helper.dart'; import 'package:afterwork/data/services/secure_storage.dart'; import 'package:afterwork/presentation/state_management/event_bloc.dart'; @@ -9,8 +10,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:http/http.dart' as http; import 'package:intl/date_symbol_data_local.dart'; import 'package:provider/provider.dart'; - import 'core/theme/theme_provider.dart'; +import 'data/providers/friends_provider.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -22,22 +23,30 @@ void main() async { final eventRemoteDataSource = EventRemoteDataSource(http.Client()); final SecureStorage secureStorage = SecureStorage(); final PreferencesHelper preferencesHelper = PreferencesHelper(); + final http.Client httpClient = http.Client(); // Nouvelle instance http.Client - // Récupération des informations stockées + // Récupération des informations stockées avec logs détaillés String? userId = await secureStorage.getUserId(); String? userName = await preferencesHelper.getUserName(); String? userLastName = await preferencesHelper.getUserLastName(); - // Gestion des valeurs par défaut si nécessaires + // Log de la récupération des informations + print("[LOG] Récupération des informations utilisateur : userId = $userId, userName = $userName, userLastName = $userLastName"); + + // Gestion des valeurs par défaut si les informations ne sont pas trouvées userId ??= 'default_user_id'; userName ??= 'Default'; userLastName ??= 'User'; + // Log des valeurs par défaut appliquées + print("[LOG] Valeurs par défaut appliquées : userId = $userId, userName = $userName, userLastName = $userLastName"); + runApp(MyApp( eventRemoteDataSource: eventRemoteDataSource, userId: userId, userName: userName, userLastName: userLastName, + httpClient: httpClient, // Passe l'instance client ici )); } @@ -46,6 +55,7 @@ class MyApp extends StatelessWidget { final String userId; final String userName; final String userLastName; + final http.Client httpClient; // Ajout de ce paramètre const MyApp({ super.key, @@ -53,17 +63,26 @@ class MyApp extends StatelessWidget { required this.userId, required this.userName, required this.userLastName, + required this.httpClient, // Spécifier l'argument }); @override Widget build(BuildContext context) { + final friendsRepository = FriendsRepositoryImpl(client: httpClient); // Utilisation du client ici + + // Log lors de la construction de l'application + print("[LOG] Construction de l'application avec userId : $userId, userName : $userName, userLastName : $userLastName"); + return MultiProvider( providers: [ ChangeNotifierProvider( create: (_) => UserProvider()..setUser(userId, userName, userLastName), ), ChangeNotifierProvider( - create: (_) => ThemeProvider(), // Fournisseur de thème + create: (_) => FriendsProvider(friendsRepository: friendsRepository), + ), + ChangeNotifierProvider( + create: (_) => ThemeProvider(), ), BlocProvider( create: (context) => EventBloc(remoteDataSource: eventRemoteDataSource), diff --git a/lib/presentation/screens/event/event_card.dart b/lib/presentation/screens/event/event_card.dart index d57fc92..7067ea9 100644 --- a/lib/presentation/screens/event/event_card.dart +++ b/lib/presentation/screens/event/event_card.dart @@ -77,6 +77,7 @@ class EventCard extends StatelessWidget { menuKey: menuKey, menuContext: context, location: event.location, + onClose: () { }, ), const Divider(color: Colors.white24), Row( diff --git a/lib/presentation/screens/event/event_screen.dart b/lib/presentation/screens/event/event_screen.dart index 895b194..a121a71 100644 --- a/lib/presentation/screens/event/event_screen.dart +++ b/lib/presentation/screens/event/event_screen.dart @@ -1,9 +1,9 @@ +import 'package:afterwork/presentation/screens/event/event_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:afterwork/data/models/event_model.dart'; -import 'package:afterwork/presentation/screens/event/event_card.dart'; -import '../dialogs/add_event_dialog.dart'; + import '../../state_management/event_bloc.dart'; +import '../dialogs/add_event_dialog.dart'; class EventScreen extends StatefulWidget { final String userId; @@ -42,7 +42,8 @@ class _EventScreenState extends State { backgroundColor: const Color(0xFF1E1E2C), actions: [ IconButton( - icon: const Icon(Icons.add_circle_outline, size: 28, color: Color(0xFF1DBF73)), + icon: const Icon(Icons.add_circle_outline, + size: 28, color: Color(0xFF1DBF73)), onPressed: () { // Naviguer vers une nouvelle page pour ajouter un événement Navigator.push( @@ -95,7 +96,6 @@ class _EventScreenState extends State { }, status: event.status, ); - }, ); } else if (state is EventError) { diff --git a/lib/presentation/screens/friends/friends_content.dart b/lib/presentation/screens/friends/friends_content.dart new file mode 100644 index 0000000..fe30a54 --- /dev/null +++ b/lib/presentation/screens/friends/friends_content.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +import '../../widgets/friend_card.dart'; +import '../../widgets/friend_detail_screen.dart'; + +class FriendsContent extends StatelessWidget { + final List> friends = [ + {'name': 'Alice', 'imageUrl': 'https://example.com/image1.jpg'}, + {'name': 'Bob', 'imageUrl': 'https://example.com/image2.jpg'}, + {'name': 'Charlie', 'imageUrl': 'https://example.com/image3.jpg'}, + // Autres amis... + ]; + + @override + Widget build(BuildContext context) { + return ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0), + itemCount: friends.length, + itemBuilder: (context, index) { + final friend = friends[index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: FriendCard( + name: friend['name']!, + imageUrl: friend['imageUrl']!, + onTap: () => _navigateToFriendDetail(context, friend), + ), + ); + }, + ); + } + + void _navigateToFriendDetail(BuildContext context, Map friend) { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => FriendDetailScreen( + name: friend['name']!, + imageUrl: friend['imageUrl']!, + friendId: friend['friendId']!, + ), + )); + } +} diff --git a/lib/presentation/screens/friends/friends_screen.dart b/lib/presentation/screens/friends/friends_screen.dart new file mode 100644 index 0000000..0c8a871 --- /dev/null +++ b/lib/presentation/screens/friends/friends_screen.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../../data/providers/friends_provider.dart'; +import '../../widgets/friend_detail_screen.dart'; +import '../../widgets/friends_circle.dart'; +import '../../widgets/search_friends.dart'; + +/// [FriendsScreen] est l'écran principal permettant d'afficher et de gérer la liste des amis. +/// Il inclut des fonctionnalités de pagination, de recherche, et de rafraîchissement manuel de la liste. +class FriendsScreen extends StatefulWidget { + final String userId; // Identifiant de l'utilisateur pour récupérer ses amis + + const FriendsScreen({Key? key, required this.userId}) : super(key: key); + + @override + _FriendsScreenState createState() => _FriendsScreenState(); +} + +class _FriendsScreenState extends State { + late ScrollController _scrollController; + + @override + void initState() { + super.initState(); + // Initialisation du contrôleur de défilement pour la gestion de la pagination. + _scrollController = ScrollController(); + _scrollController.addListener(_onScroll); + + // Log pour indiquer le début du chargement des amis + debugPrint("[LOG] Initialisation de la page : chargement des amis pour l'utilisateur ${widget.userId}"); + // Chargement initial de la liste d'amis + Provider.of(context, listen: false).fetchFriends(widget.userId); + } + + @override + void dispose() { + // Nettoyage du contrôleur de défilement pour éviter les fuites de mémoire. + _scrollController.removeListener(_onScroll); + _scrollController.dispose(); + super.dispose(); + debugPrint("[LOG] Dispose : contrôleur de défilement supprimé"); + } + + /// Méthode déclenchée lors du défilement de la liste. + /// Vérifie si l'utilisateur a atteint le bas de la liste pour charger plus d'amis. + void _onScroll() { + final provider = Provider.of(context, listen: false); + if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent && + !provider.isLoading && provider.hasMore) { + debugPrint("[LOG] Scroll : fin de liste atteinte, chargement de la page suivante"); + // Charger plus d'amis si on atteint la fin de la liste + provider.fetchFriends(widget.userId); + } + } + + @override + Widget build(BuildContext context) { + // Accès au fournisseur pour gérer les données et les états des amis. + final friendsProvider = Provider.of(context, listen: false); + + return Scaffold( + appBar: AppBar( + title: const Text('Mes Amis'), + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () { + // Log de l'action de rafraîchissement + debugPrint("[LOG] Bouton Refresh : demande de rafraîchissement de la liste des amis"); + // Rafraîchir la liste des amis + friendsProvider.fetchFriends(widget.userId); + }, + ), + ], + ), + body: SafeArea( + child: Column( + children: [ + const Padding( + padding: EdgeInsets.all(8.0), + // Widget pour la recherche d'amis + child: SearchFriends(), + ), + Expanded( + // Construction de la liste d'amis basée sur l'état du FriendsProvider + child: Consumer( + builder: (context, friendsProvider, child) { + // Si le chargement est en cours et qu'il n'y a aucun ami, afficher un indicateur de chargement. + if (friendsProvider.isLoading && friendsProvider.friendsList.isEmpty) { + debugPrint("[LOG] Chargement : affichage de l'indicateur de progression"); + return const Center(child: CircularProgressIndicator()); + } + + // Si la liste est vide après le chargement, afficher un message indiquant qu'aucun ami n'a été trouvé. + if (friendsProvider.friendsList.isEmpty) { + debugPrint("[LOG] Liste vide : Aucun ami trouvé"); + return const Center( + child: Text('Aucun ami trouvé'), + ); + } + + // Affichage de la grille des amis + debugPrint("[LOG] Affichage de la grille des amis (nombre d'amis : ${friendsProvider.friendsList.length})"); + + return GridView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(16), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + ), + itemCount: friendsProvider.friendsList.length, + itemBuilder: (context, index) { + final friend = friendsProvider.friendsList[index]; + debugPrint("[LOG] Affichage de l'ami à l'index $index avec ID : ${friend.friendId}"); + + return FriendsCircle( + friend: friend, + onTap: () { + // Log pour l'action de visualisation des détails d'un ami + debugPrint("[LOG] Détail : Affichage des détails de l'ami ID : ${friend.friendId}"); + // Naviguer vers l'écran des détails de l'ami + FriendDetailScreen.open( + context, + friend.friendId, + friend.firstName ?? 'Ami inconnu', + friend.imageUrl ?? '', + ); + }, + ); + }, + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/screens/friends/friends_screen_with_provider.dart b/lib/presentation/screens/friends/friends_screen_with_provider.dart new file mode 100644 index 0000000..e36f650 --- /dev/null +++ b/lib/presentation/screens/friends/friends_screen_with_provider.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../../assets/animations/friend_expanding_card.dart'; +import '../../../data/providers/friends_provider.dart'; +import '../../../domain/entities/friend.dart'; +import '../../widgets/friend_detail_screen.dart'; +import '../../widgets/friends_appbar.dart'; +import '../../widgets/search_friends.dart'; + +/// [FriendsScreenWithProvider] est un écran qui affiche la liste des amis. +/// Il utilise le provider [FriendsProvider] pour gérer les états et les données. +/// Chaque action est loguée pour permettre une traçabilité complète. +class FriendsScreenWithProvider extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: FriendsAppBar(), + body: SafeArea( + child: Column( + children: [ + const Padding( + padding: EdgeInsets.all(8.0), + child: SearchFriends(), + ), + Expanded( + child: Consumer( + builder: (context, friendsProvider, _) { + final friends = friendsProvider.friendsList; + + if (friends.isEmpty) { + return const Center( + child: Text( + 'Aucun ami trouvé', + style: TextStyle(color: Colors.white70), + ), + ); + } + + return ListView.builder( + physics: const BouncingScrollPhysics(), + itemCount: friends.length, + itemBuilder: (context, index) { + final friend = friends[index]; + return Dismissible( + key: Key(friend.friendId), + background: Container( + color: Colors.redAccent, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 20), + child: const Icon(Icons.delete, color: Colors.white), + ), + onDismissed: (direction) { + debugPrint("[LOG] Suppression de l'ami avec l'ID : ${friend.friendId}"); + friendsProvider.removeFriend(friend.friendId); + }, + child: FriendExpandingCard( + name: friend.firstName ?? 'Ami inconnu', + imageUrl: friend.imageUrl ?? '', + description: "Amis depuis ${friend.friendId}", + onTap: () => _navigateToFriendDetail(context, friend), + onMessageTap: () { + debugPrint("[LOG] Envoi d'un message à l'ami : ${friend.firstName ?? 'Ami inconnu'}"); + }, + onRemoveTap: () { + debugPrint("[LOG] Tentative de suppression de l'ami : ${friend.firstName ?? 'Ami inconnu'}"); + friendsProvider.removeFriend(friend.friendId); + }, + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ); + } + + /// Navigue vers l'écran des détails de l'utilisateur (ami) récupéré via son `friendId`. + void _navigateToFriendDetail(BuildContext context, Friend friend) { + debugPrint("[LOG] Navigation vers les détails de l'ami : ${friend.firstName ?? 'Ami inconnu'}"); + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => FriendDetailScreen( + name: friend.firstName ?? 'Ami inconnu', + imageUrl: friend.imageUrl ?? '', + friendId: friend.friendId, // Passer l'ID pour récupérer les détails complets + ), + )); + } +} diff --git a/lib/presentation/screens/home/home_screen.dart b/lib/presentation/screens/home/home_screen.dart index d375a26..c116f4e 100644 --- a/lib/presentation/screens/home/home_screen.dart +++ b/lib/presentation/screens/home/home_screen.dart @@ -6,17 +6,17 @@ import 'package:afterwork/presentation/screens/social/social_screen.dart'; import 'package:afterwork/presentation/screens/establishments/establishments_screen.dart'; import 'package:afterwork/presentation/screens/home/home_content.dart'; import 'package:afterwork/data/datasources/event_remote_data_source.dart'; -import 'package:afterwork/presentation/screens/notifications/notifications_screen.dart'; // Importez l'écran de notifications - +import 'package:afterwork/presentation/screens/notifications/notifications_screen.dart'; // Écran de notifications import '../../../core/constants/colors.dart'; -import '../../../core/theme/theme_provider.dart'; // Pour basculer le thème +import '../../../core/theme/theme_provider.dart'; +import '../friends/friends_screen.dart'; // Écran des amis class HomeScreen extends StatefulWidget { final EventRemoteDataSource eventRemoteDataSource; final String userId; final String userName; final String userLastName; - final String userProfileImage; // Ajouter un champ pour l'image de profil de l'utilisateur + final String userProfileImage; // Image de profil de l'utilisateur const HomeScreen({ Key? key, @@ -69,54 +69,53 @@ class _HomeScreenState extends State with SingleTickerProviderStateM headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ SliverAppBar( - backgroundColor: AppColors.backgroundColor, // Gère dynamiquement la couleur d'arrière-plan + backgroundColor: AppColors.backgroundColor, floating: true, pinned: true, snap: true, - elevation: 2, // Réduction de l'élévation pour un design plus léger + elevation: 2, leading: Padding( - padding: const EdgeInsets.all(4.0), // Réduction du padding + padding: const EdgeInsets.all(4.0), // Ajustement du padding child: Image.asset( 'lib/assets/images/logo.png', - height: 40, // Taille réduite du logo + height: 40, // Taille ajustée du logo ), ), actions: [ _buildActionIcon(Icons.add, 'Publier', context), _buildActionIcon(Icons.search, 'Rechercher', context), _buildActionIcon(Icons.message, 'Message', context), - _buildNotificationsIcon(context, 45), + _buildNotificationsIcon(context, 5), // Gérer la logique des notifications ici - // Ajout du bouton pour basculer entre les thèmes + // Bouton pour basculer entre les thèmes Switch( value: themeProvider.isDarkMode, onChanged: (value) { - themeProvider.toggleTheme(); // Bascule le thème lorsqu'on clique + themeProvider.toggleTheme(); // Changer le thème }, activeColor: AppColors.accentColor, ), ], bottom: TabBar( controller: _tabController, - indicatorColor: AppColors.lightPrimary, // Tab active en bleu + indicatorColor: AppColors.lightPrimary, labelStyle: const TextStyle( - fontSize: 12, // Réduction de la taille du texte des onglets + fontSize: 12, // Taille réduite du texte fontWeight: FontWeight.w500, ), unselectedLabelStyle: const TextStyle( - fontSize: 11, // Réduction pour les onglets non sélectionnés + fontSize: 11, // Taille ajustée pour les onglets non sélectionnés ), - // Changement des couleurs pour les tabs non sélectionnées et sélectionnées - labelColor: AppColors.lightPrimary, // Tab active en bleu - unselectedLabelColor: AppColors.iconSecondary, // Tabs non sélectionnées en blanc + labelColor: AppColors.lightPrimary, + unselectedLabelColor: AppColors.iconSecondary, tabs: [ const Tab(icon: Icon(Icons.home, size: 24), text: 'Accueil'), const Tab(icon: Icon(Icons.event, size: 24), text: 'Événements'), const Tab(icon: Icon(Icons.location_city, size: 24), text: 'Établissements'), const Tab(icon: Icon(Icons.people, size: 24), text: 'Social'), - const Tab(icon: Icon(Icons.notifications, size: 24), text: 'Notifications'), - _buildProfileTab(), + const Tab(icon: Icon(Icons.people_alt_outlined, size: 24), text: 'Ami(e)s'), + _buildProfileTab(), // Onglet profil ], ), ), @@ -133,7 +132,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ), const EstablishmentsScreen(), const SocialScreen(), - const NotificationsScreen(), + FriendsScreen(userId: widget.userId), // Correction ici : passer l'userId const ProfileScreen(), ], ), @@ -141,27 +140,26 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ); } - // Widget pour afficher la photo de profil de l'utilisateur dans l'onglet + // Widget pour l'affichage de la photo de profil dans l'onglet Tab _buildProfileTab() { return Tab( child: Container( decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( - color: Colors.blue, // Définir la couleur de la bordure ici + color: Colors.blue, width: 2.0, ), ), child: CircleAvatar( - radius: 16, // Ajustez la taille si nécessaire - backgroundColor: Colors.grey[200], // Couleur de fond pour le cas où l'image ne charge pas + radius: 16, + backgroundColor: Colors.grey[200], // Couleur de fond par défaut child: ClipOval( child: FadeInImage.assetNetwork( - placeholder: 'lib/assets/images/user_placeholder.png', // Chemin de l'image par défaut + placeholder: 'lib/assets/images/user_placeholder.png', image: widget.userProfileImage, fit: BoxFit.cover, imageErrorBuilder: (context, error, stackTrace) { - // Si l'image ne charge pas, afficher une image par défaut return Image.asset('lib/assets/images/profile_picture.png', fit: BoxFit.cover); }, ), @@ -171,12 +169,12 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ); } - // Widget pour afficher l'icône de notifications avec un badge si nécessaire + // Icône pour les notifications avec un badge Widget _buildNotificationsIcon(BuildContext context, int notificationCount) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 6.0), child: Stack( - clipBehavior: Clip.none, // Permet de positionner le badge en dehors des limites du Stack + clipBehavior: Clip.none, // Permet d'afficher le badge en dehors des limites children: [ CircleAvatar( backgroundColor: AppColors.surface, @@ -184,7 +182,6 @@ class _HomeScreenState extends State with SingleTickerProviderStateM child: IconButton( icon: const Icon(Icons.notifications, color: AppColors.darkOnPrimary, size: 20), onPressed: () { - // Rediriger vers l'écran des notifications Navigator.push( context, MaterialPageRoute( @@ -194,7 +191,6 @@ class _HomeScreenState extends State with SingleTickerProviderStateM }, ), ), - // Affiche le badge si le nombre de notifications est supérieur à 0 if (notificationCount > 0) Positioned( right: -6, @@ -202,7 +198,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM child: Container( padding: const EdgeInsets.all(2), decoration: const BoxDecoration( - color: Colors.red, // Couleur du badge + color: Colors.red, shape: BoxShape.circle, ), constraints: const BoxConstraints( @@ -210,7 +206,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM minHeight: 18, ), child: Text( - notificationCount > 99 ? '99+' : '$notificationCount', // Affiche "99+" si le nombre dépasse 99 + notificationCount > 99 ? '99+' : '$notificationCount', style: const TextStyle( color: Colors.white, fontSize: 10, @@ -225,14 +221,15 @@ class _HomeScreenState extends State with SingleTickerProviderStateM ); } + // Icône d'action générique Widget _buildActionIcon(IconData iconData, String label, BuildContext context) { return Padding( - padding: const EdgeInsets.symmetric(horizontal: 6.0), // Réduction de l'espacement + padding: const EdgeInsets.symmetric(horizontal: 6.0), child: CircleAvatar( backgroundColor: AppColors.surface, - radius: 18, // Réduction de la taille des avatars + radius: 18, child: IconButton( - icon: Icon(iconData, color: AppColors.darkOnPrimary, size: 20), // Taille réduite de l'icône + icon: Icon(iconData, color: AppColors.darkOnPrimary, size: 20), onPressed: () { _onMenuSelected(context, label); }, diff --git a/lib/presentation/screens/login/login_screen.dart b/lib/presentation/screens/login/login_screen.dart index 40d4649..69def17 100644 --- a/lib/presentation/screens/login/login_screen.dart +++ b/lib/presentation/screens/login/login_screen.dart @@ -10,13 +10,14 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:http/http.dart' as http; import 'package:loading_icon_button/loading_icon_button.dart'; import 'package:provider/provider.dart'; + +import '../../../core/errors/exceptions.dart'; import '../../../core/theme/theme_provider.dart'; import '../../../data/datasources/event_remote_data_source.dart'; import '../signup/SignUpScreen.dart'; -/// Écran de connexion pour l'application AfterWork. -/// Ce fichier contient des fonctionnalités comme la gestion de la connexion, -/// l'authentification avec mot de passe en clair, la gestion des erreurs et un thème jour/nuit. +/// L'écran de connexion où les utilisateurs peuvent s'authentifier. +/// Toutes les actions sont loguées pour permettre un suivi dans le terminal et détecter les erreurs. class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -24,19 +25,21 @@ class LoginScreen extends StatefulWidget { _LoginScreenState createState() => _LoginScreenState(); } -class _LoginScreenState extends State with SingleTickerProviderStateMixin { - final _formKey = GlobalKey(); // Clé pour valider le formulaire de connexion. +class _LoginScreenState extends State + with SingleTickerProviderStateMixin { + // Clé globale pour la validation du formulaire + final _formKey = GlobalKey(); - // Champs utilisateur - String _email = ''; // Email de l'utilisateur - String _password = ''; // Mot de passe de l'utilisateur + // Variables pour stocker l'email et le mot de passe saisis par l'utilisateur + String _email = ''; + String _password = ''; - // États de gestion - bool _isPasswordVisible = false; // Pour afficher/masquer le mot de passe - bool _isSubmitting = false; // Indicateur pour l'état de soumission du formulaire - bool _showErrorMessage = false; // Affichage des erreurs + // États de l'écran + bool _isPasswordVisible = false; // Pour basculer la visibilité du mot de passe + bool _isSubmitting = false; // Indique si la soumission du formulaire est en cours + bool _showErrorMessage = false; // Affiche un message d'erreur si nécessaire - // Services pour les opérations + // Sources de données et services final UserRemoteDataSource _userRemoteDataSource = UserRemoteDataSource(http.Client()); final SecureStorage _secureStorage = SecureStorage(); final PreferencesHelper _preferencesHelper = PreferencesHelper(); @@ -44,7 +47,7 @@ class _LoginScreenState extends State with SingleTickerProviderStat // Contrôleur pour le bouton de chargement final _btnController = LoadingButtonController(); - // Contrôleur d'animation pour la transition des écrans + // Contrôleur d'animation pour gérer la transition entre les écrans late AnimationController _animationController; @override @@ -54,25 +57,25 @@ class _LoginScreenState extends State with SingleTickerProviderStat vsync: this, duration: const Duration(milliseconds: 500), ); - print("Contrôleur d'animation initialisé."); + debugPrint("[LOG] Contrôleur d'animation initialisé."); } @override void dispose() { _animationController.dispose(); - print("Ressources d'animation libérées."); + debugPrint("[LOG] Ressources d'animation libérées."); super.dispose(); } - /// Fonction pour basculer la visibilité du mot de passe + /// Bascule la visibilité du mot de passe et logue l'état actuel. void _togglePasswordVisibility() { setState(() { _isPasswordVisible = !_isPasswordVisible; }); - print("Visibilité du mot de passe basculée: $_isPasswordVisible"); + debugPrint("[LOG] Visibilité du mot de passe basculée: $_isPasswordVisible"); } - /// Fonction pour afficher un toast via FlutterToast + /// Affiche un toast avec le message spécifié et logue l'action. void _showToast(String message) { Fluttertoast.showToast( msg: message, @@ -83,60 +86,79 @@ class _LoginScreenState extends State with SingleTickerProviderStat textColor: Colors.white, fontSize: 16.0, ); + debugPrint("[LOG] Toast affiché : $message"); } - /// Fonction soumettre le formulaire + /// Soumet le formulaire de connexion et tente d'authentifier l'utilisateur. + /// Toutes les étapes et erreurs sont loguées pour une traçabilité complète. Future _submit() async { - print("Tentative de soumission du formulaire de connexion."); + debugPrint("[LOG] Tentative de soumission du formulaire de connexion."); if (_formKey.currentState!.validate()) { setState(() { _isSubmitting = true; _showErrorMessage = false; }); - _formKey.currentState!.save(); + _formKey.currentState!.save(); // Sauvegarde des données saisies try { - _btnController.start(); + _btnController.start(); // Démarre l'animation de chargement du bouton + debugPrint("[LOG] Appel à l'API pour authentifier l'utilisateur."); + + // Appel à l'API pour authentifier l'utilisateur final UserModel user = await _userRemoteDataSource.authenticateUser(_email, _password); - if (user == null) { - throw Exception("L'utilisateur n'a pas été trouvé ou l'authentification a échoué."); - } - print("Utilisateur authentifié : ${user.userId}"); - await _secureStorage.saveUserId(user.userId); - await _preferencesHelper.saveUserName(user.nom); - await _preferencesHelper.saveUserLastName(user.prenoms); - _showToast("Connexion réussie !"); + // Validation de l'ID utilisateur + if (user.userId.isNotEmpty) { + debugPrint("[LOG] Utilisateur authentifié avec succès. ID: ${user.userId}"); - // Navigation vers la page d'accueil - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => HomeScreen( - eventRemoteDataSource: EventRemoteDataSource(http.Client()), - userId: user.userId, - userName: user.nom, - userLastName: user.prenoms, - userProfileImage: 'lib/assets/images/profile_picture.png', + // Sauvegarde des informations utilisateur + await _secureStorage.saveUserId(user.userId); + await _preferencesHelper.saveUserName(user.nom); + await _preferencesHelper.saveUserLastName(user.prenoms); + + _showToast("Connexion réussie !"); + // Redirection vers l'écran d'accueil + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => HomeScreen( + userId: user.userId, + userName: user.nom, + userLastName: user.prenoms, + userProfileImage: 'lib/assets/images/profile_picture.png', + eventRemoteDataSource: EventRemoteDataSource(http.Client()), + ), ), - ), - ); + ); + } else { + debugPrint("[ERROR] L'ID utilisateur est manquant dans la réponse."); + _showToast("Erreur : ID utilisateur manquant."); + } } catch (e) { - print("Erreur lors de l'authentification : $e"); - _btnController.error(); - _showToast("Erreur lors de la connexion : ${e.toString()}"); + // Gestion des erreurs spécifiques et log de chaque type d'erreur + if (e is ServerExceptionWithMessage) { + debugPrint("[ERROR] Erreur serveur : ${e.message}"); + _showToast("Erreur serveur : ${e.message}"); + } else if (e is UnauthorizedException) { + debugPrint("[ERROR] Erreur d'authentification : ${e.message}"); + _showToast("Erreur : ${e.message}"); + } else { + debugPrint("[ERROR] Erreur lors de la connexion : $e"); + _showToast("Erreur lors de la connexion : ${e.toString()}"); + } + _btnController.error(); // Affiche une erreur sur le bouton setState(() { _showErrorMessage = true; }); } finally { - _btnController.reset(); + _btnController.reset(); // Réinitialise l'état du bouton setState(() { - _isSubmitting = false; + _isSubmitting = false; // Réinitialise l'état de chargement }); } } else { - print("Échec de validation du formulaire."); + debugPrint("[ERROR] Validation du formulaire échouée."); _btnController.reset(); _showToast("Veuillez vérifier les informations saisies."); } @@ -152,19 +174,21 @@ class _LoginScreenState extends State with SingleTickerProviderStat return Scaffold( body: Stack( children: [ + // Arrière-plan animé AnimatedContainer( duration: const Duration(seconds: 3), decoration: BoxDecoration( gradient: LinearGradient( colors: [ theme.colorScheme.primary, - theme.colorScheme.secondary + theme.colorScheme.secondary, ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), ), + // Spinner de chargement lors de la soumission if (_isSubmitting) const Center( child: SpinKitFadingCircle( @@ -172,6 +196,7 @@ class _LoginScreenState extends State with SingleTickerProviderStat size: 50.0, ), ), + // Icône de changement de thème Positioned( top: 40, right: 20, @@ -182,10 +207,11 @@ class _LoginScreenState extends State with SingleTickerProviderStat ), onPressed: () { themeProvider.toggleTheme(); - print("Thème basculé : ${themeProvider.isDarkMode ? 'Sombre' : 'Clair'}"); + debugPrint("[LOG] Thème basculé : ${themeProvider.isDarkMode ? 'Sombre' : 'Clair'}"); }, ), ), + // Formulaire de connexion Center( child: SingleChildScrollView( padding: const EdgeInsets.all(16.0), @@ -209,18 +235,18 @@ class _LoginScreenState extends State with SingleTickerProviderStat icon: Icons.email, validator: (value) { if (value == null || value.isEmpty) { - print("Erreur : champ email vide."); + debugPrint("[ERROR] Champ email vide."); return 'Veuillez entrer votre email'; } if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) { - print("Erreur : email invalide."); + debugPrint("[ERROR] Email invalide."); return 'Veuillez entrer un email valide'; } return null; }, onSaved: (value) { _email = value!; - print("Email enregistré : $_email"); + debugPrint("[LOG] Email enregistré : $_email"); }, ), const SizedBox(height: 20), @@ -230,27 +256,25 @@ class _LoginScreenState extends State with SingleTickerProviderStat obscureText: !_isPasswordVisible, suffixIcon: IconButton( icon: Icon( - _isPasswordVisible - ? Icons.visibility - : Icons.visibility_off, + _isPasswordVisible ? Icons.visibility : Icons.visibility_off, color: theme.iconTheme.color, ), onPressed: _togglePasswordVisibility, ), validator: (value) { if (value == null || value.isEmpty) { - print("Erreur : champ mot de passe vide."); + debugPrint("[ERROR] Champ mot de passe vide."); return 'Veuillez entrer votre mot de passe'; } if (value.length < 6) { - print("Erreur : mot de passe trop court."); + debugPrint("[ERROR] Mot de passe trop court."); return 'Le mot de passe doit comporter au moins 6 caractères'; } return null; }, onSaved: (value) { _password = value!; - print("Mot de passe enregistré."); + debugPrint("[LOG] Mot de passe enregistré."); }, ), const SizedBox(height: 30), @@ -272,28 +296,26 @@ class _LoginScreenState extends State with SingleTickerProviderStat const SizedBox(height: 20), TextButton( onPressed: () { - print("Redirection vers la page d'inscription"); + debugPrint("[LOG] Redirection vers la page d'inscription."); Navigator.push( context, MaterialPageRoute( - builder: (context) => SignUpScreen(), + builder: (context) => const SignUpScreen(), ), ); }, child: Text( 'Pas encore de compte ? Inscrivez-vous', - style: theme.textTheme.bodyMedium! - .copyWith(color: Colors.white70), + style: theme.textTheme.bodyMedium!.copyWith(color: Colors.white70), ), ), TextButton( onPressed: () { - print("Mot de passe oublié"); + debugPrint("[LOG] Mot de passe oublié cliqué."); }, child: Text( 'Mot de passe oublié ?', - style: theme.textTheme.bodyMedium! - .copyWith(color: Colors.white70), + style: theme.textTheme.bodyMedium!.copyWith(color: Colors.white70), ), ), if (_showErrorMessage) @@ -326,9 +348,8 @@ class _LoginScreenState extends State with SingleTickerProviderStat ), if (isKeyboardVisible) Text( - '© 2024 LionsDev', - style: theme.textTheme.bodyMedium! - .copyWith(color: Colors.white70), + '© 2024', + style: theme.textTheme.bodyMedium!.copyWith(color: Colors.white70), textAlign: TextAlign.center, ), ], @@ -339,7 +360,7 @@ class _LoginScreenState extends State with SingleTickerProviderStat ); } - /// Widget réutilisable pour les champs de texte avec validation et design amélioré + /// Méthode pour construire les champs de formulaire avec les styles adaptés. Widget _buildTextFormField({ required String label, required IconData icon, diff --git a/lib/presentation/screens/signup/SignUpScreen.dart b/lib/presentation/screens/signup/SignUpScreen.dart index be3d741..212eedf 100644 --- a/lib/presentation/screens/signup/SignUpScreen.dart +++ b/lib/presentation/screens/signup/SignUpScreen.dart @@ -89,9 +89,6 @@ class _SignUpScreenState extends State { // Envoi des informations pour créer un nouvel utilisateur final createdUser = await _userRemoteDataSource.createUser(user); - if (createdUser == null) { - throw Exception("La création du compte a échoué."); - } print("Utilisateur créé : ${createdUser.userId}"); diff --git a/lib/presentation/screens/social/social_card.dart b/lib/presentation/screens/social/social_card.dart new file mode 100644 index 0000000..3b8afc4 --- /dev/null +++ b/lib/presentation/screens/social/social_card.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import '../../../core/constants/colors.dart'; +import '../../../data/models/social_post_model.dart'; +import '../../widgets/social_header_widget.dart'; +import '../../widgets/social_interaction_row.dart'; +import '../../widgets/swipe_background.dart'; // Import du widget de swipe + +class SocialCard extends StatelessWidget { + final SocialPost post; + final VoidCallback onLike; + final VoidCallback onComment; + final VoidCallback onShare; + final VoidCallback onDeletePost; + final VoidCallback onEditPost; + + const SocialCard({ + Key? key, + required this.post, + required this.onLike, + required this.onComment, + required this.onShare, + required this.onDeletePost, + required this.onEditPost, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Dismissible( + key: ValueKey(post.postText), + direction: DismissDirection.endToStart, + onDismissed: (direction) { + onDeletePost(); + }, + background: SwipeBackground( + color: Colors.red, + icon: Icons.delete, + label: 'Supprimer', + ), + child: Card( + color: AppColors.cardColor, + margin: const EdgeInsets.symmetric(vertical: 10.0), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SocialHeaderWidget( + post: post, + onEditPost: () { + print('Modifier le post'); + }, + menuKey: GlobalKey(), + menuContext: context, + onClosePost: () { + print('Close post'); + }, + ), + const SizedBox(height: 8), + Text ( + post.postText, + style: TextStyle( + color: AppColors.textSecondary, + fontSize: 14, + ), + ), + const SizedBox(height: 8), + if (post.postImage.isNotEmpty) + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset(post.postImage, fit: BoxFit.cover), + ), + const SizedBox(height: 8), + Row( + children: post.tags + .map((tag) => Padding( + padding: const EdgeInsets.only(right: 8), + child: Text( + tag, + style: TextStyle( + color: AppColors.accentColor, + fontSize: 12, + ), + ), + )) + .toList(), + ), + const SizedBox(height: 8), + SocialInteractionRow( + post: post, + onLike: onLike, + onComment: onComment, + onShare: onShare, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/screens/social/social_content.dart b/lib/presentation/screens/social/social_content.dart new file mode 100644 index 0000000..4d6735f --- /dev/null +++ b/lib/presentation/screens/social/social_content.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import '../../../data/models/social_post_model.dart'; +import 'social_card.dart'; // Import de la SocialCard + +class SocialContent extends StatefulWidget { + const SocialContent({super.key}); + + @override + _SocialContentState createState() => _SocialContentState(); +} + +class _SocialContentState extends State { + final List _posts = [ + SocialPost( + userName: 'John Doe', + userImage: 'lib/assets/images/profile_picture.png', + postText: 'Une belle journée au parc avec des amis ! 🌳🌞', + postImage: 'lib/assets/images/placeholder.png', + likes: 12, + comments: 4, + badges: ['Explorer', 'Photographe'], + tags: ['#Nature', '#FunDay'], + shares: 25, + ), + SocialPost( + userName: 'Jane Smith', + userImage: 'lib/assets/images/profile_picture.png', + postText: 'Mon nouveau chat est tellement mignon 🐱', + postImage: 'lib/assets/images/placeholder.png', + likes: 30, + comments: 8, + badges: ['Animal Lover', 'Partageur'], + tags: ['#Chat', '#Cuteness'], + shares: 25, + ), + SocialPost( + userName: 'Alice Brown', + userImage: 'lib/assets/images/profile_picture.png', + postText: 'Café du matin avec une vue magnifique ☕️', + postImage: 'lib/assets/images/placeholder.png', + likes: 45, + comments: 15, + badges: ['Gourmet', 'Partageur'], + tags: ['#Café', '#MorningVibes'], + shares: 25, + ), + ]; + + @override + Widget build(BuildContext context) { + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: _posts.length, + itemBuilder: (context, index) { + final post = _posts[index]; + return SocialCard( + post: post, + onLike: () { + setState(() { + _posts[index] = SocialPost( + userName: post.userName, + userImage: post.userImage, + postText: post.postText, + postImage: post.postImage, + likes: post.likes + 1, + comments: post.comments, + badges: post.badges, + tags: post.tags, + shares: post.shares + 1, + ); + }); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Like ajouté')), + ); + }, + onComment: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Commentaire ajouté')), + ); + }, + onShare: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Post partagé')), + ); + }, + onDeletePost: () { + setState(() { + _posts.removeAt(index); + }); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Post supprimé')), + ); + }, + onEditPost: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Post modifié')), + ); + }, + ); + }, + ); + } +} diff --git a/lib/presentation/screens/social/social_screen.dart b/lib/presentation/screens/social/social_screen.dart index 9c4813e..5c071e2 100644 --- a/lib/presentation/screens/social/social_screen.dart +++ b/lib/presentation/screens/social/social_screen.dart @@ -1,19 +1,25 @@ import 'package:flutter/material.dart'; +import 'social_content.dart'; // Import du fichier qui contient SocialContent class SocialScreen extends StatelessWidget { const SocialScreen({super.key}); @override Widget build(BuildContext context) { - return Center( - child: Text( - 'Social', - style: TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, + return Scaffold( + backgroundColor: const Color(0xFF1E1E2C), // Fond noir pour correspondre à un thème sombre + appBar: AppBar( + title: const Text( + 'Social', + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), ), + backgroundColor: Colors.black, // AppBar avec fond noir pour un design cohérent ), + body: SocialContent(), // Appel à SocialContent pour afficher le contenu ); } } diff --git a/lib/presentation/widgets/event_header.dart b/lib/presentation/widgets/event_header.dart index 81ab9f9..71c0f41 100644 --- a/lib/presentation/widgets/event_header.dart +++ b/lib/presentation/widgets/event_header.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:afterwork/core/utils/date_formatter.dart'; -import 'event_status_badge.dart'; import 'event_menu.dart'; class EventHeader extends StatelessWidget { @@ -8,9 +7,10 @@ class EventHeader extends StatelessWidget { final String userLastName; final String? eventDate; final String? imageUrl; - final String location; // Ajout du paramètre "location" pour le lieu de l'événement + final String location; final GlobalKey menuKey; final BuildContext menuContext; + final VoidCallback onClose; // Ajout d'un callback pour l'action de fermeture const EventHeader({ Key? key, @@ -18,9 +18,10 @@ class EventHeader extends StatelessWidget { required this.userLastName, this.eventDate, this.imageUrl, - required this.location, // Initialisation de "location" + required this.location, required this.menuKey, required this.menuContext, + required this.onClose, // Initialisation du callback de fermeture }) : super(key: key); @override @@ -33,68 +34,85 @@ class EventHeader extends StatelessWidget { } String formattedDate = date != null ? DateFormatter.formatDate(date) : 'Date inconnue'; - return Row( + return Stack( children: [ - CircleAvatar( - backgroundColor: Colors.grey.shade800, - backgroundImage: imageUrl != null && imageUrl!.isNotEmpty - ? NetworkImage(imageUrl!) - : const AssetImage('lib/assets/images/placeholder.png') as ImageProvider, - radius: 22, - ), - const SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '$userName $userLastName', - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - Text( - formattedDate, - style: const TextStyle( - color: Colors.white54, - fontSize: 12, - ), - ), - const SizedBox(height: 4), - // Utilisation de Row pour afficher le lieu sur la même ligne - Row( + Row( + children: [ + CircleAvatar( + backgroundColor: Colors.grey.shade800, + backgroundImage: imageUrl != null && imageUrl!.isNotEmpty + ? NetworkImage(imageUrl!) + : const AssetImage('lib/assets/images/placeholder.png') as ImageProvider, + radius: 22, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Text( - location.isNotEmpty ? location : 'Lieu non spécifié', - style: const TextStyle( - color: Colors.white60, - fontSize: 12, - fontStyle: FontStyle.italic, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + Text( + '$userName $userLastName', + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + formattedDate, + style: const TextStyle( + color: Colors.white54, + fontSize: 12, ), ), - const SizedBox(width: 8), + const SizedBox(height: 4), + Row( + children: [ + Expanded( + child: Text( + location.isNotEmpty ? location : 'Lieu non spécifié', + style: const TextStyle( + color: Colors.white60, + fontSize: 12, + fontStyle: FontStyle.italic, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + ], + ), ], ), + ), + ], + ), + // Ajout des boutons dans le coin supérieur droit + Positioned( + top: 0, + right: 0, + child: Row( + children: [ + IconButton( + key: menuKey, + icon: const Icon(Icons.more_vert, color: Colors.white54, size: 20), + splashRadius: 20, + onPressed: () { + showEventOptions(menuContext, menuKey); + }, + ), + IconButton( + icon: const Icon(Icons.close, color: Colors.white54, size: 20), + splashRadius: 20, + onPressed: onClose, // Appel du callback de fermeture + ), ], ), ), - IconButton( - key: menuKey, - icon: const Icon(Icons.more_vert, color: Colors.white54, size: 20), - splashRadius: 20, - onPressed: () { - showEventOptions(menuContext, menuKey); - }, - ), ], ); } diff --git a/lib/presentation/widgets/friend_card.dart b/lib/presentation/widgets/friend_card.dart new file mode 100644 index 0000000..9b0ac89 --- /dev/null +++ b/lib/presentation/widgets/friend_card.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; + +/// [FriendCard] est un widget représentant une carte d'ami. +/// Cette carte inclut l'image de l'ami, son nom, et un bouton qui permet +/// d'interagir avec cette carte (via le `onTap`). +/// +/// Ce widget est conçu pour être utilisé dans des listes d'amis, comme +/// dans la section "Mes Amis" de l'application. +class FriendCard extends StatelessWidget { + final String name; // Le nom de l'ami + final String imageUrl; // URL de l'image de profil de l'ami + final VoidCallback onTap; // Fonction callback exécutée lors d'un clic sur la carte + + /// Constructeur de [FriendCard] avec des paramètres obligatoires. + const FriendCard({ + Key? key, + required this.name, + required this.imageUrl, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + // Lorsque l'utilisateur clique sur la carte, on déclenche la fonction onTap. + debugPrint("[LOG] Carte de l'ami $name cliquée."); + onTap(); // Exécuter le callback fourni + }, + child: Card( + elevation: 4, // Élévation de la carte pour donner un effet d'ombre + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)), // Bordure arrondie + color: Colors.grey.shade800, // Couleur de fond de la carte + child: Padding( + padding: const EdgeInsets.all(12.0), // Padding interne pour espacer le contenu + child: Row( + children: [ + // Image de profil de l'ami affichée sous forme de cercle + Hero( + tag: name, // Le tag Hero permet de créer une transition animée vers un autre écran. + child: CircleAvatar( + backgroundImage: NetworkImage(imageUrl), // Charger l'image depuis l'URL + radius: 30, // Taille de l'avatar + ), + ), + const SizedBox(width: 16), // Espacement entre l'image et le nom + // Le nom de l'ami avec un texte en gras et blanc + Expanded( + child: Text( + name, + style: const TextStyle( + fontSize: 18, // Taille de la police + color: Colors.white, // Couleur du texte + fontWeight: FontWeight.bold, // Style en gras + ), + ), + ), + // Icône de flèche indiquant que la carte est cliquable + Icon(Icons.chevron_right, color: Colors.white70), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/friend_detail_screen.dart b/lib/presentation/widgets/friend_detail_screen.dart new file mode 100644 index 0000000..1635219 --- /dev/null +++ b/lib/presentation/widgets/friend_detail_screen.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:logger/logger.dart'; + +/// [FriendDetailScreen] affiche les détails d'un ami, incluant son nom, son image de profil, +/// et une option pour envoyer un message. +/// +/// Utilisé lorsque l'utilisateur clique sur un ami pour voir plus de détails. +class FriendDetailScreen extends StatelessWidget { + final String name; // Nom de l'ami + final String imageUrl; // URL de l'image de profil de l'ami + final String friendId; // ID de l'ami pour des actions futures + final Logger _logger = Logger(); // Logger pour suivre les actions dans le terminal + + /// Constructeur de la classe [FriendDetailScreen]. + /// [name], [imageUrl], et [friendId] doivent être fournis. + FriendDetailScreen({ + Key? key, + required this.name, + required this.imageUrl, + required this.friendId, + }) : super(key: key); + + /// Méthode statique pour lancer l'écran des détails d'un ami. + static void open(BuildContext context, String friendId, String name, String imageUrl) { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => FriendDetailScreen( + friendId: friendId, + name: name, + imageUrl: imageUrl, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + _logger.i('[LOG] Affichage des détails de l\'ami : $name (ID: $friendId)'); + + // Utilise `AssetImage` si `imageUrl` est vide ou ne contient pas d'URL valide. + final imageProvider = imageUrl.isNotEmpty && Uri.tryParse(imageUrl)?.hasAbsolutePath == true + ? NetworkImage(imageUrl) + : const AssetImage('lib/assets/images/default_avatar.png') as ImageProvider; + + return Scaffold( + appBar: AppBar( + title: Text(name), // Titre de l'écran affichant le nom de l'ami + backgroundColor: Colors.grey.shade800, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), // Espacement autour du contenu + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Affichage de l'image de l'ami avec animation `Hero` + Hero( + tag: friendId, // Tag unique pour l'animation Hero basée sur l'ID de l'ami + child: CircleAvatar( + radius: 50, + backgroundImage: imageProvider, + backgroundColor: Colors.grey.shade800, + onBackgroundImageError: (error, stackTrace) { + _logger.e('[ERROR] Erreur lors du chargement de l\'image pour $name (ID: $friendId): $error'); + }, + child: imageUrl.isEmpty + ? const Icon(Icons.person, size: 50, color: Colors.white) // Icône par défaut si aucune image n'est disponible + : null, + ), + ), + const SizedBox(height: 16), // Espacement entre l'image et le texte + + // Affichage du nom de l'ami + Text( + name, + style: const TextStyle( + fontSize: 24, // Taille de la police pour le nom + fontWeight: FontWeight.bold, // Texte en gras + color: Colors.white, + ), + ), + const SizedBox(height: 20), // Espacement avant le bouton + + // Bouton pour envoyer un message à l'ami + ElevatedButton.icon( + onPressed: () { + _logger.i('[LOG] Envoi d\'un message à $name (ID: $friendId)'); + // Logique future pour envoyer un message à l'ami + }, + icon: const Icon(Icons.message), + label: const Text('Envoyer un message'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.teal, // Couleur de fond du bouton + foregroundColor: Colors.white, // Couleur du texte et de l'icône + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/friends_appbar.dart b/lib/presentation/widgets/friends_appbar.dart new file mode 100644 index 0000000..3d3a4eb --- /dev/null +++ b/lib/presentation/widgets/friends_appbar.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:logger/logger.dart'; + +/// [FriendsAppBar] est une barre d'application personnalisée utilisée dans l'écran des amis. +/// Elle permet d'ajouter et de gérer les amis avec des actions spécifiques. +/// Toutes les actions sont loguées pour une traçabilité complète. +class FriendsAppBar extends StatelessWidget implements PreferredSizeWidget { + final Logger _logger = Logger(); // Logger pour tracer toutes les actions + + FriendsAppBar({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppBar( + backgroundColor: Colors.grey.shade800, // Couleur de fond de la barre d'application + title: const Text( + 'Mes Amis', // Titre de l'écran + style: TextStyle(color: Colors.white), // Couleur du texte + ), + actions: [ + Tooltip( + message: 'Ajouter un ami', // Améliore l'accessibilité pour l'icône d'ajout + child: IconButton( + icon: const Icon(Icons.group_add, color: Colors.white), // Icône pour ajouter un nouvel ami + onPressed: () { + _logger.i("[LOG] Bouton 'Ajouter un ami' pressé."); + // Logique à implémenter pour ajouter un nouvel ami + }, + ), + ), + Tooltip( + message: 'Gérer les groupes d\'amis', // Améliore l'accessibilité pour l'icône de gestion des groupes + child: IconButton( + icon: const Icon(Icons.group, color: Colors.white), // Icône pour gérer les groupes d'amis + onPressed: () { + _logger.i("[LOG] Bouton 'Gérer les groupes' pressé."); + // Logique à implémenter pour gérer les groupes d'amis + }, + ), + ), + ], + ); + } + + /// Définit la taille préférée de la barre d'application. + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} diff --git a/lib/presentation/widgets/friends_circle.dart b/lib/presentation/widgets/friends_circle.dart new file mode 100644 index 0000000..5d74df8 --- /dev/null +++ b/lib/presentation/widgets/friends_circle.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:logger/logger.dart'; +import '../../domain/entities/friend.dart'; + +/// [FriendsCircle] est un widget qui affiche un ami sous forme d'avatar circulaire avec son nom. +/// L'avatar est cliquable, permettant à l'utilisateur d'accéder aux détails de l'ami +/// ou de déclencher d'autres actions liées. +class FriendsCircle extends StatelessWidget { + final Friend friend; // Représente l'entité Friend à afficher (nom et image). + final VoidCallback onTap; // Fonction callback exécutée lorsque l'utilisateur clique sur l'avatar. + + // Logger pour tracer les actions dans le terminal + final Logger _logger = Logger(); + + /// Constructeur pour [FriendsCircle], prenant en entrée un ami et une fonction de callback. + FriendsCircle({ + Key? key, + required this.friend, // L'ami à afficher (doit inclure friendId, name, imageUrl). + required this.onTap, // Action à exécuter lors du clic. + }) : super(key: key); + + @override + Widget build(BuildContext context) { + // Combine firstName et lastName ou utilise "Ami inconnu" par défaut. + String displayName = [friend.firstName, friend.lastName] + .where((namePart) => namePart != null && namePart.isNotEmpty) + .join(" ") + .trim(); + + if (displayName.isEmpty) { + displayName = 'Ami inconnu'; + } + + return GestureDetector( + onTap: () { + _logger.i('[LOG] Avatar de ${displayName.trim()} cliqué'); + onTap(); // Exécute l'action de clic définie par l'utilisateur + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, // Centre verticalement les éléments de la colonne. + children: [ + Hero( + tag: friend.friendId, // Tag unique pour l'animation Hero basé sur l'ID de l'ami. + child: CircleAvatar( + radius: 40, + backgroundImage: friend.imageUrl != null && friend.imageUrl!.isNotEmpty + ? NetworkImage(friend.imageUrl!) // Utilise NetworkImage si l'URL est valide + : AssetImage('lib/assets/images/default_avatar.png') as ImageProvider, // Utilise AssetImage pour l'avatar par défaut + onBackgroundImageError: (error, stackTrace) { + _logger.e('[ERROR] Erreur lors du chargement de l\'image pour ${displayName.trim()} : $error'); + }, + backgroundColor: Colors.grey.shade800, // Fond si l'image ne charge pas. + child: friend.imageUrl == null || friend.imageUrl!.isEmpty + ? const Icon(Icons.person, size: 40, color: Colors.white) // Icône de remplacement si aucune image n'est disponible + : null, + ), + ), + const SizedBox(height: 8), // Ajoute un espace entre l'image et le texte. + Text( + displayName, // Affiche le nom de l'ami sous l'avatar ou une valeur par défaut. + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/search_friends.dart b/lib/presentation/widgets/search_friends.dart new file mode 100644 index 0000000..d7bd576 --- /dev/null +++ b/lib/presentation/widgets/search_friends.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +/// [SearchFriends] est un widget permettant à l'utilisateur de rechercher des amis. +/// Il inclut un champ de texte stylisé pour saisir la requête de recherche. +/// Chaque modification du texte dans le champ génère un log dans le terminal pour suivre en temps réel l'activité. +class SearchFriends extends StatelessWidget { + const SearchFriends({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return TextField( + style: const TextStyle( + color: Colors.white, // Le texte saisi est de couleur blanche. + ), + decoration: InputDecoration( + hintText: 'Rechercher un ami...', // Indication textuelle pour aider l'utilisateur. + hintStyle: const TextStyle( + color: Colors.white54, // Style de l'indicateur avec une couleur plus claire. + ), + filled: true, + fillColor: Colors.grey.shade800, // Couleur de fond du champ de recherche. + prefixIcon: const Icon( + Icons.search, // Icône de loupe pour indiquer la recherche. + color: Colors.white54, // Couleur de l'icône de recherche. + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(30.0), // Bordure arrondie pour un style moderne. + borderSide: BorderSide.none, // Aucune bordure visible pour un look propre. + ), + ), + onChanged: (value) { + // Fonction appelée chaque fois que l'utilisateur modifie le texte dans le champ de recherche. + debugPrint('[LOG] Recherche d\'amis : $value'); // Log de chaque saisie. + // Vous pouvez ajouter ici la logique de filtrage de la liste des amis en fonction de la recherche. + }, + ); + } +} diff --git a/lib/presentation/widgets/social_badge_widget.dart b/lib/presentation/widgets/social_badge_widget.dart new file mode 100644 index 0000000..a0032aa --- /dev/null +++ b/lib/presentation/widgets/social_badge_widget.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import '../../../core/constants/colors.dart'; + +class BadgeWidget extends StatelessWidget { + final String badge; + final IconData? icon; // Optionnel : ajouter une icône au badge + + const BadgeWidget({ + Key? key, + required this.badge, + this.icon, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + decoration: BoxDecoration( + color: AppColors.accentColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(12.0), + border: Border.all( + color: AppColors.accentColor, + width: 1.0, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) ...[ + Icon( + icon, + color: AppColors.accentColor, + size: 16.0, + ), + const SizedBox(width: 5), + ], + Text( + badge, + style: TextStyle( + color: AppColors.accentColor, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/social_header_widget.dart b/lib/presentation/widgets/social_header_widget.dart new file mode 100644 index 0000000..62bc8f7 --- /dev/null +++ b/lib/presentation/widgets/social_header_widget.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import '../../../core/constants/colors.dart'; +import '../../../data/models/social_post_model.dart'; +import 'social_badge_widget.dart'; // Import du widget BadgeWidget + +class SocialHeaderWidget extends StatelessWidget { + final SocialPost post; + final VoidCallback onEditPost; + final VoidCallback onClosePost; // Ajout du callback pour la fermeture du post + final GlobalKey menuKey; + final BuildContext menuContext; + + const SocialHeaderWidget({ + Key? key, + required this.post, + required this.onEditPost, + required this.onClosePost, // Initialisation du callback de fermeture + required this.menuKey, + required this.menuContext, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Row( + children: [ + CircleAvatar( + backgroundColor: Colors.grey.shade800, + backgroundImage: post.userImage.isNotEmpty + ? AssetImage(post.userImage) + : const AssetImage('lib/assets/images/placeholder.png'), + radius: 22, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + post.userName, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Wrap( + spacing: 6, + children: post.badges + .map((badge) => BadgeWidget(badge: badge)) + .toList(), + ), + ], + ), + ), + ], + ), + // Ajout des boutons dans le coin supérieur droit + Positioned( + top: 0, + right: 0, + child: Row( + mainAxisSize: MainAxisSize.min, // Réduit la taille du Row au minimum + children: [ + IconButton( + key: menuKey, + icon: const Icon(Icons.more_vert, color: Colors.white54, size: 20), + splashRadius: 20, + onPressed: () { + _showOptionsMenu(menuContext, menuKey); + }, + ), + const SizedBox(width: 4), // Espacement entre les boutons + IconButton( + icon: const Icon(Icons.close, color: Colors.white54, size: 20), + splashRadius: 20, + onPressed: onClosePost, // Appel du callback de fermeture + ), + ], + ), + ), + ], + ); + } + + void _showOptionsMenu(BuildContext context, GlobalKey menuKey) { + showModalBottomSheet( + context: context, + builder: (context) { + return Container( + color: AppColors.backgroundColor, + child: Wrap( + children: [ + ListTile( + leading: Icon(Icons.edit, color: AppColors.iconPrimary), + title: const Text('Modifier'), + onTap: () { + Navigator.of(context).pop(); + onEditPost(); + }, + ), + ListTile( + leading: Icon(Icons.delete, color: AppColors.iconPrimary), + title: const Text('Supprimer'), + onTap: () { + Navigator.of(context).pop(); + }, + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/presentation/widgets/social_interaction_row.dart b/lib/presentation/widgets/social_interaction_row.dart new file mode 100644 index 0000000..0022b8a --- /dev/null +++ b/lib/presentation/widgets/social_interaction_row.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import '../../../core/constants/colors.dart'; +import '../../../data/models/social_post_model.dart'; + +class SocialInteractionRow extends StatelessWidget { + final SocialPost post; + final VoidCallback onLike; + final VoidCallback onComment; + final VoidCallback onShare; + + const SocialInteractionRow({ + Key? key, + required this.post, + required this.onLike, + required this.onComment, + required this.onShare, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: _buildIconButton(Icons.thumb_up_alt_outlined, 'J’aime', post.likes, onLike), + ), + Expanded( + child: _buildIconButton(Icons.comment_outlined, 'Commentaires', post.comments, onComment), + ), + Expanded( + child: _buildIconButton(Icons.share_outlined, 'Partages', post.shares, onShare), + ), + ], + ); + } + + Widget _buildIconButton(IconData icon, String label, int count, VoidCallback onPressed) { + return TextButton.icon( + onPressed: onPressed, + icon: Icon(icon, color: AppColors.accentColor, size: 18), + label: Text( + '$label ($count)', + style: const TextStyle(color: Colors.white70, fontSize: 12), + overflow: TextOverflow.ellipsis, + ), + ); + } +} diff --git a/lib/presentation/widgets/submit_button.dart b/lib/presentation/widgets/submit_button.dart index bf5edbb..7c4cdeb 100644 --- a/lib/presentation/widgets/submit_button.dart +++ b/lib/presentation/widgets/submit_button.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:afterwork/core/constants/colors.dart'; class SubmitButton extends StatelessWidget { final VoidCallback onPressed; diff --git a/pubspec.lock b/pubspec.lock index 54b82d9..136701f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,18 +13,18 @@ packages: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" asn1lib: dependency: transitive description: name: asn1lib - sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70" + sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" url: "https://pub.dev" source: hosted - version: "1.5.5" + version: "1.5.8" async: dependency: transitive description: @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.4.2" clock: dependency: transitive description: @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" cross_file: dependency: transitive description: @@ -141,18 +141,18 @@ packages: dependency: transitive description: name: crypto - sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" csslib: dependency: transitive description: name: csslib - sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -205,26 +205,26 @@ packages: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" file_selector_linux: dependency: transitive description: name: file_selector_linux - sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + sha256: "712ce7fab537ba532c8febdb1a8f167b32441e74acd68c3ccb2e36dcb52c4ab2" url: "https://pub.dev" source: hosted - version: "0.9.2+1" + version: "0.9.3" file_selector_macos: dependency: transitive description: name: file_selector_macos - sha256: cb284e267f8e2a45a904b5c094d2ba51d0aabfc20b1538ab786d9ef7dc2bf75c + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" url: "https://pub.dev" source: hosted - version: "0.9.4+1" + version: "0.9.4+2" file_selector_platform_interface: dependency: transitive description: @@ -237,10 +237,10 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69" + sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" url: "https://pub.dev" source: hosted - version: "0.9.3+2" + version: "0.9.3+3" flare_flutter: dependency: "direct main" description: @@ -282,10 +282,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" + sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.0.23" flutter_secure_storage: dependency: "direct main" description: @@ -380,10 +380,10 @@ packages: dependency: transitive description: name: google_maps - sha256: "463b38e5a92a05cde41220a11fd5eef3847031fef3e8cf295ac76ec453246907" + sha256: "4d6e199c561ca06792c964fa24b2bac7197bf4b401c2e1d23e345e5f9939f531" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "8.1.1" google_maps_flutter: dependency: "direct main" description: @@ -396,26 +396,26 @@ packages: dependency: transitive description: name: google_maps_flutter_android - sha256: "10cf27bee8c560f8e69992b3a0f27ddf1d7acbea622ddb13ef3f587848a73f26" + sha256: bccf64ccbb2ea672dc62a61177b315a340af86b0228564484b023657544a3fd5 url: "https://pub.dev" source: hosted - version: "2.14.7" + version: "2.14.11" google_maps_flutter_ios: dependency: transitive description: name: google_maps_flutter_ios - sha256: "3a484846fc56f15e47e3de1f5ea80a7ff2b31721d2faa88f390f3b3cf580c953" + sha256: "753ebf6a2bc24c5eba8e714c901345d858abd9694b1f878c43614fd3f06b8060" url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.13.1" google_maps_flutter_platform_interface: dependency: transitive description: name: google_maps_flutter_platform_interface - sha256: "099874463dc4c9bff04fe4b2b8cf7284d2455c2deead8f9a59a87e1b9f028c69" + sha256: a951981c22d790848efb9f114f81794945bc5c06bc566238a419a92f110af6cb url: "https://pub.dev" source: hosted - version: "2.9.2" + version: "2.9.5" google_maps_flutter_web: dependency: transitive description: @@ -428,10 +428,10 @@ packages: dependency: transitive description: name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" url: "https://pub.dev" source: hosted - version: "0.15.4" + version: "0.15.5" http: dependency: "direct main" description: @@ -452,10 +452,10 @@ packages: dependency: transitive description: name: image - sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.3.0" image_picker: dependency: "direct main" description: @@ -468,10 +468,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: c0a6763d50b354793d0192afd0a12560b823147d3ded7c6b77daf658fa05cc85 + sha256: "8faba09ba361d4b246dc0a17cb4289b3324c2b9f6db7b3d457ee69106a86bd32" url: "https://pub.dev" source: hosted - version: "0.8.12+13" + version: "0.8.12+17" image_picker_for_web: dependency: transitive description: @@ -484,10 +484,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" + sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b" url: "https://pub.dev" source: hosted - version: "0.8.12" + version: "0.8.12+1" image_picker_linux: dependency: transitive description: @@ -572,10 +572,10 @@ packages: dependency: "direct main" description: name: loading_icon_button - sha256: "5ef8c82796c19b96a5995457410037cbe05cb4840af766e50330b2d108dacdfd" + sha256: "682c03eaaeb6d00e3cce318ec365c0190eeacbc56ff1b6694a4561ed1e3b7e02" url: "https://pub.dev" source: hosted - version: "0.0.6" + version: "0.0.7" logger: dependency: "direct main" description: @@ -609,7 +609,7 @@ packages: source: hosted version: "1.0.4" meta: - dependency: transitive + dependency: "direct main" description: name: meta sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 @@ -644,18 +644,18 @@ packages: dependency: "direct main" description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a url: "https://pub.dev" source: hosted - version: "2.2.10" + version: "2.2.12" path_provider_foundation: dependency: transitive description: @@ -740,10 +740,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -796,18 +796,18 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_linux: dependency: transitive description: @@ -905,10 +905,10 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" vector_math: dependency: transitive description: @@ -921,42 +921,42 @@ packages: dependency: "direct main" description: name: video_player - sha256: e30df0d226c4ef82e2c150ebf6834b3522cf3f654d8e2f9419d376cdc071425d + sha256: "4a8c3492d734f7c39c2588a3206707a05ee80cef52e8c7f3b2078d430c84bc17" url: "https://pub.dev" source: hosted - version: "2.9.1" + version: "2.9.2" video_player_android: dependency: transitive description: name: video_player_android - sha256: "38d8fe136c427abdce68b5e8c3c08ea29d7a794b453c7a51b12ecfad4aad9437" + sha256: "391e092ba4abe2f93b3e625bd6b6a6ec7d7414279462c1c0ee42b5ab8d0a0898" url: "https://pub.dev" source: hosted - version: "2.7.3" + version: "2.7.16" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c + sha256: cd5ab8a8bc0eab65ab0cea40304097edc46da574c8c1ecdee96f28cd8ef3792f url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.6.2" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" + sha256: "229d7642ccd9f3dc4aba169609dd6b5f3f443bb4cc15b82f7785fcada5af9bbb" url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.2.3" video_player_web: dependency: transitive description: name: video_player_web - sha256: "6dcdd298136523eaf7dfc31abaf0dfba9aa8a8dbc96670e87e9d42b6f2caf774" + sha256: "881b375a934d8ebf868c7fb1423b2bfaa393a0a265fa3f733079a86536064a10" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" vm_service: dependency: transitive description: @@ -969,18 +969,18 @@ packages: dependency: transitive description: name: web - sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2cb6a95..bcfa571 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,7 @@ dependencies: # Functional programming utilities dartz: ^0.10.1 + meta: any dependency_overrides: rxdart: ^0.28.0 @@ -82,6 +83,7 @@ flutter: - lib/assets/images/story_placeholder.png - lib/assets/images/user_placeholder.png - lib/assets/videos/test.mp4 + - lib/assets/images/default_avatar.png fonts: - family: Montserrat