From 75c12e35a5fdcc0241c9adb8a982132c2cf5f46f Mon Sep 17 00:00:00 2001 From: Van Leemput Dayron Date: Wed, 5 Nov 2025 09:31:58 +0100 Subject: [PATCH] Add UserStateWrapper and ProfileImageService for user state management and profile image handling --- .../activities/add_activity_bottom_sheet.dart | 14 +- lib/components/profile/profile_content.dart | 36 ++- lib/components/widgets/user_state_widget.dart | 42 ++++ lib/services/activity_places_service.dart | 206 +++++++++++------- lib/services/profile_image_service.dart | 54 +++++ lib/services/trip_image_service.dart | 54 ++--- 6 files changed, 275 insertions(+), 131 deletions(-) create mode 100644 lib/components/widgets/user_state_widget.dart create mode 100644 lib/services/profile_image_service.dart diff --git a/lib/components/activities/add_activity_bottom_sheet.dart b/lib/components/activities/add_activity_bottom_sheet.dart index e211a7b..dd86c6f 100644 --- a/lib/components/activities/add_activity_bottom_sheet.dart +++ b/lib/components/activities/add_activity_bottom_sheet.dart @@ -237,16 +237,16 @@ class _AddActivityBottomSheetState extends State { borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: isDarkMode - ? Colors.white.withOpacity(0.2) - : Colors.black.withOpacity(0.2), + ? Colors.white.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.2), ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: isDarkMode - ? Colors.white.withOpacity(0.2) - : Colors.black.withOpacity(0.2), + ? Colors.white.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.2), ), ), focusedBorder: OutlineInputBorder( @@ -267,7 +267,9 @@ class _AddActivityBottomSheetState extends State { decoration: BoxDecoration( color: theme.colorScheme.surface, borderRadius: BorderRadius.circular(12), - border: Border.all(color: theme.colorScheme.outline.withOpacity(0.5)), + border: Border.all( + color: theme.colorScheme.outline.withValues(alpha: 0.5), + ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -275,7 +277,7 @@ class _AddActivityBottomSheetState extends State { Text( 'Sélectionnez une catégorie', style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface.withOpacity(0.7), + color: theme.colorScheme.onSurface.withValues(alpha: 0.7), ), ), const SizedBox(height: 12), diff --git a/lib/components/profile/profile_content.dart b/lib/components/profile/profile_content.dart index 6086dd0..65f1a6f 100644 --- a/lib/components/profile/profile_content.dart +++ b/lib/components/profile/profile_content.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:travel_mate/components/widgets/user_state_widget.dart'; import '../../blocs/user/user_bloc.dart'; import '../../blocs/user/user_state.dart' as user_state; import '../../blocs/user/user_event.dart' as user_event; @@ -10,22 +11,8 @@ class ProfileContent extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state is user_state.UserLoading) { - return Center(child: CircularProgressIndicator()); - } - - if (state is user_state.UserError) { - return Center(child: Text('Erreur: ${state.message}')); - } - - if (state is! user_state.UserLoaded) { - return Center(child: Text('Aucun utilisateur connecté')); - } - - final user = state.user; - + return UserStateWrapper( + builder: (context, user) { return Column( children: [ // Section titre @@ -181,7 +168,10 @@ class ProfileContent extends StatelessWidget { ); } - void _showChangePasswordDialog(BuildContext context, user_state.UserModel user) { + void _showChangePasswordDialog( + BuildContext context, + user_state.UserModel user, + ) { final currentPasswordController = TextEditingController(); final newPasswordController = TextEditingController(); final confirmPasswordController = TextEditingController(); @@ -210,7 +200,9 @@ class ProfileContent extends StatelessWidget { TextField( controller: confirmPasswordController, obscureText: true, - decoration: InputDecoration(labelText: 'Confirmer le mot de passe'), + decoration: InputDecoration( + labelText: 'Confirmer le mot de passe', + ), ), ], ), @@ -233,7 +225,8 @@ class ProfileContent extends StatelessWidget { return; } - if (newPasswordController.text != confirmPasswordController.text) { + if (newPasswordController.text != + confirmPasswordController.text) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Les mots de passe ne correspondent pas'), @@ -274,7 +267,10 @@ class ProfileContent extends StatelessWidget { ); } - void _showDeleteAccountDialog(BuildContext context, user_state.UserModel user) { + void _showDeleteAccountDialog( + BuildContext context, + user_state.UserModel user, + ) { final passwordController = TextEditingController(); final authService = AuthService(); diff --git a/lib/components/widgets/user_state_widget.dart b/lib/components/widgets/user_state_widget.dart new file mode 100644 index 0000000..632c5f9 --- /dev/null +++ b/lib/components/widgets/user_state_widget.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../blocs/user/user_bloc.dart'; +import '../../blocs/user/user_state.dart' as user_state; + +class UserStateWrapper extends StatelessWidget { + final Widget Function(BuildContext context, dynamic user) builder; + final Widget? loadingWidget; + final Widget? errorWidget; + final Widget? noUserWidget; + + const UserStateWrapper({ + super.key, + required this.builder, + this.loadingWidget, + this.errorWidget, + this.noUserWidget, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is user_state.UserLoading) { + return loadingWidget ?? + const Center(child: CircularProgressIndicator()); + } + + if (state is user_state.UserError) { + return errorWidget ?? Center(child: Text('Erreur: ${state.message}')); + } + + if (state is! user_state.UserLoaded) { + return noUserWidget ?? + const Center(child: Text('Aucun utilisateur connecté.')); + } + + return builder(context, state.user); + }, + ); + } +} diff --git a/lib/services/activity_places_service.dart b/lib/services/activity_places_service.dart index 615c611..b028fa0 100644 --- a/lib/services/activity_places_service.dart +++ b/lib/services/activity_places_service.dart @@ -6,7 +6,8 @@ import '../services/error_service.dart'; /// Service pour rechercher des activités touristiques via Google Places API class ActivityPlacesService { - static final ActivityPlacesService _instance = ActivityPlacesService._internal(); + static final ActivityPlacesService _instance = + ActivityPlacesService._internal(); factory ActivityPlacesService() => _instance; ActivityPlacesService._internal(); @@ -23,14 +24,16 @@ class ActivityPlacesService { int offset = 0, }) async { try { - print('ActivityPlacesService: Recherche d\'activités pour: $destination (max: $maxResults, offset: $offset)'); + print( + 'ActivityPlacesService: Recherche d\'activités pour: $destination (max: $maxResults, offset: $offset)', + ); // 1. Géocoder la destination final coordinates = await _geocodeDestination(destination); // 2. Rechercher les activités par catégorie ou toutes les catégories List allActivities = []; - + if (category != null) { final activities = await _searchByCategory( coordinates['lat']!, @@ -66,22 +69,30 @@ class ActivityPlacesService { final uniqueActivities = _removeDuplicates(allActivities); uniqueActivities.sort((a, b) => (b.rating ?? 0).compareTo(a.rating ?? 0)); - print('ActivityPlacesService: ${uniqueActivities.length} activités trouvées au total'); - + print( + 'ActivityPlacesService: ${uniqueActivities.length} activités trouvées au total', + ); + // 4. Appliquer la pagination final startIndex = offset; - final endIndex = (startIndex + maxResults).clamp(0, uniqueActivities.length); - + final endIndex = (startIndex + maxResults).clamp( + 0, + uniqueActivities.length, + ); + if (startIndex >= uniqueActivities.length) { - print('ActivityPlacesService: Offset $startIndex dépasse le nombre total (${uniqueActivities.length})'); + print( + 'ActivityPlacesService: Offset $startIndex dépasse le nombre total (${uniqueActivities.length})', + ); return []; } - - final paginatedResults = uniqueActivities.sublist(startIndex, endIndex); - print('ActivityPlacesService: Retour de ${paginatedResults.length} activités (offset: $offset, max: $maxResults)'); - - return paginatedResults; + final paginatedResults = uniqueActivities.sublist(startIndex, endIndex); + print( + 'ActivityPlacesService: Retour de ${paginatedResults.length} activités (offset: $offset, max: $maxResults)', + ); + + return paginatedResults; } catch (e) { print('ActivityPlacesService: Erreur lors de la recherche: $e'); _errorService.logError('activity_places_service', e); @@ -99,19 +110,20 @@ class ActivityPlacesService { } final encodedDestination = Uri.encodeComponent(destination); - final url = 'https://maps.googleapis.com/maps/api/geocode/json?address=$encodedDestination&key=$_apiKey'; - + final url = + 'https://maps.googleapis.com/maps/api/geocode/json?address=$encodedDestination&key=$_apiKey'; + print('ActivityPlacesService: Géocodage de "$destination"'); print('ActivityPlacesService: URL = $url'); - + final response = await http.get(Uri.parse(url)); - + print('ActivityPlacesService: Status code = ${response.statusCode}'); - + if (response.statusCode == 200) { final data = json.decode(response.body); print('ActivityPlacesService: Réponse géocodage = ${data['status']}'); - + if (data['status'] == 'OK' && data['results'].isNotEmpty) { final location = data['results'][0]['geometry']['location']; final coordinates = { @@ -121,13 +133,17 @@ class ActivityPlacesService { print('ActivityPlacesService: Coordonnées trouvées = $coordinates'); return coordinates; } else { - print('ActivityPlacesService: Erreur API = ${data['error_message'] ?? data['status']}'); + print( + 'ActivityPlacesService: Erreur API = ${data['error_message'] ?? data['status']}', + ); if (data['status'] == 'REQUEST_DENIED') { - throw Exception('🔑 Clé API non autorisée. Activez les APIs suivantes dans Google Cloud Console:\n' - '• Geocoding API\n' - '• Places API\n' - '• Maps JavaScript API\n' - 'Puis ajoutez des restrictions appropriées.'); + throw Exception( + '🔑 Clé API non autorisée. Activez les APIs suivantes dans Google Cloud Console:\n' + '• Geocoding API\n' + '• Places API\n' + '• Maps JavaScript API\n' + 'Puis ajoutez des restrictions appropriées.', + ); } else if (data['status'] == 'ZERO_RESULTS') { throw Exception('Aucun résultat trouvé pour cette destination'); } else { @@ -139,7 +155,7 @@ class ActivityPlacesService { } } catch (e) { print('ActivityPlacesService: Erreur géocodage: $e'); - throw e; // Rethrow pour permettre la gestion d'erreur en amont + rethrow; // Rethrow pour permettre la gestion d'erreur en amont } } @@ -152,23 +168,28 @@ class ActivityPlacesService { int radius, ) async { try { - final url = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json' + final url = + 'https://maps.googleapis.com/maps/api/place/nearbysearch/json' '?location=$lat,$lng' '&radius=$radius' '&type=${category.googlePlaceType}' '&key=$_apiKey'; final response = await http.get(Uri.parse(url)); - + if (response.statusCode == 200) { final data = json.decode(response.body); - + if (data['status'] == 'OK') { final List activities = []; - + for (final place in data['results']) { try { - final activity = await _convertPlaceToActivity(place, tripId, category); + final activity = await _convertPlaceToActivity( + place, + tripId, + category, + ); if (activity != null) { activities.add(activity); } @@ -176,11 +197,11 @@ class ActivityPlacesService { print('ActivityPlacesService: Erreur conversion place: $e'); } } - + return activities; } } - + return []; } catch (e) { print('ActivityPlacesService: Erreur recherche par catégorie: $e'); @@ -200,15 +221,16 @@ class ActivityPlacesService { // Récupérer les détails supplémentaires final details = await _getPlaceDetails(placeId); - + final geometry = place['geometry']?['location']; final photos = place['photos'] as List?; - + // Obtenir une image de qualité String? imageUrl; if (photos != null && photos.isNotEmpty) { final photoReference = photos.first['photo_reference']; - imageUrl = 'https://maps.googleapis.com/maps/api/place/photo' + imageUrl = + 'https://maps.googleapis.com/maps/api/place/photo' '?maxwidth=800' '&photoreference=$photoReference' '&key=$_apiKey'; @@ -218,9 +240,10 @@ class ActivityPlacesService { id: '', // Sera généré lors de la sauvegarde tripId: tripId, name: place['name'] ?? 'Activité inconnue', - description: details?['editorial_summary']?['overview'] ?? - details?['formatted_address'] ?? - 'Découvrez cette activité incontournable !', + description: + details?['editorial_summary']?['overview'] ?? + details?['formatted_address'] ?? + 'Découvrez cette activité incontournable !', category: category.displayName, imageUrl: imageUrl, rating: place['rating']?.toDouble(), @@ -236,7 +259,6 @@ class ActivityPlacesService { createdAt: DateTime.now(), updatedAt: DateTime.now(), ); - } catch (e) { print('ActivityPlacesService: Erreur conversion place: $e'); return null; @@ -246,20 +268,21 @@ class ActivityPlacesService { /// Récupère les détails d'un lieu Future?> _getPlaceDetails(String placeId) async { try { - final url = 'https://maps.googleapis.com/maps/api/place/details/json' + final url = + 'https://maps.googleapis.com/maps/api/place/details/json' '?place_id=$placeId' '&fields=formatted_address,formatted_phone_number,website,opening_hours,editorial_summary' '&key=$_apiKey'; final response = await http.get(Uri.parse(url)); - + if (response.statusCode == 200) { final data = json.decode(response.body); if (data['status'] == 'OK') { return data['result']; } } - + return null; } catch (e) { print('ActivityPlacesService: Erreur récupération détails: $e'); @@ -277,10 +300,10 @@ class ActivityPlacesService { /// Parse les heures d'ouverture List _parseOpeningHours(Map? openingHours) { if (openingHours == null) return []; - + final weekdayText = openingHours['weekday_text'] as List?; if (weekdayText == null) return []; - + return weekdayText.cast(); } @@ -303,33 +326,40 @@ class ActivityPlacesService { int radius = 5000, }) async { try { - print('ActivityPlacesService: Recherche textuelle: $query à $destination'); + print( + 'ActivityPlacesService: Recherche textuelle: $query à $destination', + ); // Géocoder la destination final coordinates = await _geocodeDestination(destination); final encodedQuery = Uri.encodeComponent(query); - final url = 'https://maps.googleapis.com/maps/api/place/textsearch/json' + final url = + 'https://maps.googleapis.com/maps/api/place/textsearch/json' '?query=$encodedQuery in $destination' '&location=${coordinates['lat']},${coordinates['lng']}' '&radius=$radius' '&key=$_apiKey'; final response = await http.get(Uri.parse(url)); - + if (response.statusCode == 200) { final data = json.decode(response.body); - + if (data['status'] == 'OK') { final List activities = []; - + for (final place in data['results']) { try { // Déterminer la catégorie basée sur les types du lieu final types = List.from(place['types'] ?? []); final category = _determineCategoryFromTypes(types); - - final activity = await _convertPlaceToActivity(place, tripId, category); + + final activity = await _convertPlaceToActivity( + place, + tripId, + category, + ); if (activity != null) { activities.add(activity); } @@ -337,11 +367,11 @@ class ActivityPlacesService { print('ActivityPlacesService: Erreur conversion place: $e'); } } - + return activities; } } - + return []; } catch (e) { print('ActivityPlacesService: Erreur recherche textuelle: $e'); @@ -358,7 +388,7 @@ class ActivityPlacesService { } } } - + // Catégories par défaut basées sur des types communs if (types.contains('restaurant') || types.contains('food')) { return ActivityCategory.restaurant; @@ -371,7 +401,7 @@ class ActivityPlacesService { } else if (types.contains('night_club') || types.contains('bar')) { return ActivityCategory.nightlife; } - + return ActivityCategory.attraction; // Par défaut } @@ -388,14 +418,18 @@ class ActivityPlacesService { }) async { try { double lat, lng; - + // Utiliser les coordonnées fournies ou géocoder la destination if (latitude != null && longitude != null) { lat = latitude; lng = longitude; - print('ActivityPlacesService: Utilisation des coordonnées pré-géolocalisées: $lat, $lng'); + print( + 'ActivityPlacesService: Utilisation des coordonnées pré-géolocalisées: $lat, $lng', + ); } else if (destination != null) { - print('ActivityPlacesService: Géolocalisation de la destination: $destination'); + print( + 'ActivityPlacesService: Géolocalisation de la destination: $destination', + ); final coordinates = await _geocodeDestination(destination); lat = coordinates['lat']!; lng = coordinates['lng']!; @@ -403,7 +437,9 @@ class ActivityPlacesService { throw Exception('Destination ou coordonnées requises'); } - print('ActivityPlacesService: Recherche paginée aux coordonnées: $lat, $lng (page: ${nextPageToken ?? "première"})'); + print( + 'ActivityPlacesService: Recherche paginée aux coordonnées: $lat, $lng (page: ${nextPageToken ?? "première"})', + ); // 2. Rechercher les activités par catégorie avec pagination if (category != null) { @@ -449,7 +485,8 @@ class ActivityPlacesService { String? nextPageToken, ) async { try { - String url = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json' + String url = + 'https://maps.googleapis.com/maps/api/place/nearbysearch/json' '?location=$lat,$lng' '&radius=$radius' '&type=${category.googlePlaceType}' @@ -460,20 +497,24 @@ class ActivityPlacesService { } final response = await http.get(Uri.parse(url)); - + if (response.statusCode == 200) { final data = json.decode(response.body); - + if (data['status'] == 'OK') { final List activities = []; final results = data['results'] as List? ?? []; - + // Limiter à pageSize résultats final limitedResults = results.take(pageSize).toList(); - + for (final place in limitedResults) { try { - final activity = await _convertPlaceToActivity(place, tripId, category); + final activity = await _convertPlaceToActivity( + place, + tripId, + category, + ); if (activity != null) { activities.add(activity); } @@ -481,7 +522,7 @@ class ActivityPlacesService { print('ActivityPlacesService: Erreur conversion place: $e'); } } - + return { 'activities': activities, 'nextPageToken': data['next_page_token'], @@ -489,7 +530,7 @@ class ActivityPlacesService { }; } } - + return { 'activities': [], 'nextPageToken': null, @@ -516,7 +557,8 @@ class ActivityPlacesService { ) async { try { // Pour toutes les catégories, on utilise une recherche plus générale - String url = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json' + String url = + 'https://maps.googleapis.com/maps/api/place/nearbysearch/json' '?location=$lat,$lng' '&radius=$radius' '&type=tourist_attraction' @@ -527,24 +569,28 @@ class ActivityPlacesService { } final response = await http.get(Uri.parse(url)); - + if (response.statusCode == 200) { final data = json.decode(response.body); - + if (data['status'] == 'OK') { final List activities = []; final results = data['results'] as List? ?? []; - + // Limiter à pageSize résultats final limitedResults = results.take(pageSize).toList(); - + for (final place in limitedResults) { try { // Déterminer la catégorie basée sur les types du lieu final types = List.from(place['types'] ?? []); final category = _determineCategoryFromTypes(types); - - final activity = await _convertPlaceToActivity(place, tripId, category); + + final activity = await _convertPlaceToActivity( + place, + tripId, + category, + ); if (activity != null) { activities.add(activity); } @@ -552,7 +598,7 @@ class ActivityPlacesService { print('ActivityPlacesService: Erreur conversion place: $e'); } } - + return { 'activities': activities, 'nextPageToken': data['next_page_token'], @@ -560,14 +606,16 @@ class ActivityPlacesService { }; } } - + return { 'activities': [], 'nextPageToken': null, 'hasMoreData': false, }; } catch (e) { - print('ActivityPlacesService: Erreur recherche toutes catégories paginée: $e'); + print( + 'ActivityPlacesService: Erreur recherche toutes catégories paginée: $e', + ); return { 'activities': [], 'nextPageToken': null, @@ -575,4 +623,4 @@ class ActivityPlacesService { }; } } -} \ No newline at end of file +} diff --git a/lib/services/profile_image_service.dart b/lib/services/profile_image_service.dart new file mode 100644 index 0000000..7832ba6 --- /dev/null +++ b/lib/services/profile_image_service.dart @@ -0,0 +1,54 @@ +import 'dart:io'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:image_picker/image_picker.dart'; +import 'error_service.dart'; + +class ProfileImageService { + static final FirebaseStorage _storage = FirebaseStorage.instance; + final ImagePicker _picker = ImagePicker(); + final ErrorService _errorService = ErrorService(); + + Future uploadCustomProfileImage( + String userId, + XFile imageFile, + ) async { + try { + final ref = _storage.ref().child('profile_images').child('$userId.jpg'); + final uploadTask = ref.putFile(File(imageFile.path)); + final snapshot = await uploadTask; + return await snapshot.ref.getDownloadURL(); + } catch (e) { + _errorService.logError( + 'ProfileImageService', + 'Erreur lors du téléchargement de l\'image de profil pour $userId: $e', + ); + return null; + } + } + + Future pickProfileImageFromGallery() async { + try { + final XFile? image = await _picker.pickImage(source: ImageSource.gallery); + return image; + } catch (e) { + _errorService.logError( + 'ProfileImageService', + 'Erreur lors de la sélection de l\'image depuis la galerie: $e', + ); + return null; + } + } + + Future takePhoto() async { + try { + final XFile? photo = await _picker.pickImage(source: ImageSource.camera); + return photo; + } catch (e) { + _errorService.logError( + 'ProfileImageService', + 'Erreur lors de la prise de photo: $e', + ); + return null; + } + } +} diff --git a/lib/services/trip_image_service.dart b/lib/services/trip_image_service.dart index 9680fa9..fdc1058 100644 --- a/lib/services/trip_image_service.dart +++ b/lib/services/trip_image_service.dart @@ -11,17 +11,17 @@ class TripImageService { /// Charge les images manquantes pour une liste de voyages Future loadMissingImages(List trips) async { - final tripsWithoutImage = trips.where( - (trip) => trip.imageUrl == null || trip.imageUrl!.isEmpty - ).toList(); + final tripsWithoutImage = trips + .where((trip) => trip.imageUrl == null || trip.imageUrl!.isEmpty) + .toList(); if (tripsWithoutImage.isEmpty) { return; - } + } for (final trip in tripsWithoutImage) { try { await _loadImageForTrip(trip); - + // Petite pause entre les requêtes pour éviter de surcharger l'API await Future.delayed(const Duration(milliseconds: 500)); } catch (e) { @@ -35,42 +35,40 @@ class TripImageService { /// Charge l'image pour un voyage spécifique Future _loadImageForTrip(Trip trip) async { - // D'abord vérifier si une image existe déjà dans le Storage - String? imageUrl = await _placeImageService.getExistingImageUrl(trip.location); - + String? imageUrl = await _placeImageService.getExistingImageUrl( + trip.location, + ); + // Si aucune image n'existe, en télécharger une nouvelle - if (imageUrl == null) { - imageUrl = await _placeImageService.getPlaceImageUrl(trip.location); - } - + imageUrl ??= await _placeImageService.getPlaceImageUrl(trip.location); + if (imageUrl != null && trip.id != null) { // Mettre à jour le voyage avec l'image (existante ou nouvelle) final updatedTrip = trip.copyWith( imageUrl: imageUrl, updatedAt: DateTime.now(), ); - + await _tripRepository.updateTrip(trip.id!, updatedTrip); - } else { - } + } else {} } /// Recharge l'image d'un voyage spécifique (force le rechargement) Future reloadImageForTrip(Trip trip) async { try { final imageUrl = await _placeImageService.getPlaceImageUrl(trip.location); - + if (imageUrl != null && trip.id != null) { final updatedTrip = trip.copyWith( imageUrl: imageUrl, updatedAt: DateTime.now(), ); - + await _tripRepository.updateTrip(trip.id!, updatedTrip); return imageUrl; } - + return null; } catch (e) { _errorService.logError( @@ -87,16 +85,15 @@ class TripImageService { // Récupérer tous les voyages de l'utilisateur final tripsStream = _tripRepository.getTripsByUserId(userId); final trips = await tripsStream.first; - + // Extraire toutes les URLs d'images utilisées final usedImageUrls = trips .where((trip) => trip.imageUrl != null && trip.imageUrl!.isNotEmpty) .map((trip) => trip.imageUrl!) .toList(); - + // Nettoyer les images inutilisées await _placeImageService.cleanupUnusedImages(usedImageUrls); - } catch (e) { _errorService.logError( 'TripImageService', @@ -110,10 +107,12 @@ class TripImageService { try { final tripsStream = _tripRepository.getTripsByUserId(userId); final trips = await tripsStream.first; - - final tripsWithImages = trips.where((trip) => trip.imageUrl != null && trip.imageUrl!.isNotEmpty).length; + + final tripsWithImages = trips + .where((trip) => trip.imageUrl != null && trip.imageUrl!.isNotEmpty) + .length; final tripsWithoutImages = trips.length - tripsWithImages; - + return { 'totalTrips': trips.length, 'tripsWithImages': tripsWithImages, @@ -121,7 +120,10 @@ class TripImageService { 'timestamp': DateTime.now().toIso8601String(), }; } catch (e) { - _errorService.logError('TripImageService', 'Erreur lors de l\'obtention des statistiques: $e'); + _errorService.logError( + 'TripImageService', + 'Erreur lors de l\'obtention des statistiques: $e', + ); return { 'error': e.toString(), 'timestamp': DateTime.now().toIso8601String(), @@ -140,4 +142,4 @@ class TripImageService { ); } } -} \ No newline at end of file +}