diff --git a/lib/components/activities/activities_page.dart b/lib/components/activities/activities_page.dart index e00c213..3c24f6a 100644 --- a/lib/components/activities/activities_page.dart +++ b/lib/components/activities/activities_page.dart @@ -10,10 +10,7 @@ import '../activities/add_activity_bottom_sheet.dart'; class ActivitiesPage extends StatefulWidget { final Trip trip; - const ActivitiesPage({ - super.key, - required this.trip, - }); + const ActivitiesPage({super.key, required this.trip}); @override State createState() => _ActivitiesPageState(); @@ -30,14 +27,17 @@ class _ActivitiesPageState extends State // Cache pour éviter de recharger les données bool _activitiesLoaded = false; bool _googleSearchPerformed = false; - + // Variables pour stocker les activités localement List _tripActivities = []; List _approvedActivities = []; bool _isLoadingTripActivities = false; - int _totalGoogleActivitiesRequested = 0; // Compteur pour les recherches progressives - bool _autoReloadInProgress = false; // Protection contre les rechargements en boucle - int _lastAutoReloadTriggerCount = 0; // Éviter de redéclencher pour le même nombre + int _totalGoogleActivitiesRequested = + 0; // Compteur pour les recherches progressives + bool _autoReloadInProgress = + false; // Protection contre les rechargements en boucle + int _lastAutoReloadTriggerCount = + 0; // Éviter de redéclencher pour le même nombre @override bool get wantKeepAlive => true; // Maintient l'état de la page @@ -46,12 +46,12 @@ class _ActivitiesPageState extends State void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); - + // Charger les activités au démarrage WidgetsBinding.instance.addPostFrameCallback((_) { _loadActivitiesIfNeeded(); }); - + // Écouter les changements d'onglets _tabController.addListener(() { if (_tabController.indexIsChanging) { @@ -124,46 +124,54 @@ class _ActivitiesPageState extends State ), ); } - + if (state is ActivityLoaded) { print('✅ Activités chargées: ${state.activities.length}'); // Stocker les activités localement WidgetsBinding.instance.addPostFrameCallback((_) { setState(() { _tripActivities = state.activities; - _approvedActivities = state.activities.where((a) => a.totalVotes > 0).toList(); + _approvedActivities = state.activities + .where((a) => a.totalVotes > 0) + .toList(); _isLoadingTripActivities = false; }); - - print('🔄 [ActivityLoaded] Activités du voyage mises à jour: ${_tripActivities.length}'); + + print( + '🔄 [ActivityLoaded] Activités du voyage mises à jour: ${_tripActivities.length}', + ); // Vérifier si on a besoin de charger plus d'activités dans les suggestions Future.delayed(const Duration(milliseconds: 500), () { - print('🚀 [ActivityLoaded] Déclenchement de la vérification auto-reload'); + print( + '🚀 [ActivityLoaded] Déclenchement de la vérification auto-reload', + ); _checkAndLoadMoreActivitiesIfNeeded(); }); }); } - + if (state is ActivitySearchResults) { print('🔍 Résultats Google: ${state.searchResults.length}'); // Déclencher l'auto-reload uniquement pour la recherche initiale (6 résultats) // et pas pour les rechargements automatiques if (state.searchResults.length <= 6 && !_autoReloadInProgress) { WidgetsBinding.instance.addPostFrameCallback((_) { - print('🎯 [ActivitySearchResults] Première recherche avec peu de résultats, vérification auto-reload'); + print( + '🎯 [ActivitySearchResults] Première recherche avec peu de résultats, vérification auto-reload', + ); Future.delayed(const Duration(milliseconds: 500), () { _checkAndLoadMoreActivitiesIfNeeded(); }); }); } } - + if (state is ActivityVoteRecorded) { print('�️ Vote enregistré pour activité: ${state.activityId}'); // Recharger les activités du voyage pour mettre à jour les votes _loadActivities(); } - + if (state is ActivityAdded) { print('✅ Activité ajoutée avec succès: ${state.activity.name}'); // Recharger automatiquement les activités du voyage @@ -196,13 +204,13 @@ class _ActivitiesPageState extends State children: [ // Barre de recherche _buildSearchBar(theme), - + // Filtres _buildFilters(theme), - + // Onglets de catégories _buildCategoryTabs(theme), - + // Contenu des onglets Expanded( child: TabBarView( @@ -296,9 +304,7 @@ class _ActivitiesPageState extends State child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( - border: Border.all( - color: theme.colorScheme.outline.withOpacity(0.5), - ), + border: Border.all(color: theme.colorScheme.outline.withOpacity(0.5)), borderRadius: BorderRadius.circular(20), color: theme.colorScheme.surface, ), @@ -383,7 +389,8 @@ class _ActivitiesPageState extends State return Container( padding: const EdgeInsets.all(16), constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.7, // Limite à 70% de l'écran + maxHeight: + MediaQuery.of(context).size.height * 0.7, // Limite à 70% de l'écran ), child: Column( mainAxisSize: MainAxisSize.min, @@ -399,19 +406,26 @@ class _ActivitiesPageState extends State Flexible( child: SingleChildScrollView( child: Column( - children: categories.map((category) => ListTile( - title: Text(category), - onTap: () { - setState(() { - _selectedCategory = category; - }); - Navigator.pop(context); - _applyFilters(); - }, - trailing: _selectedCategory == category - ? Icon(Icons.check, color: theme.colorScheme.primary) - : null, - )).toList(), + children: categories + .map( + (category) => ListTile( + title: Text(category), + onTap: () { + setState(() { + _selectedCategory = category; + }); + Navigator.pop(context); + _applyFilters(); + }, + trailing: _selectedCategory == category + ? Icon( + Icons.check, + color: theme.colorScheme.primary, + ) + : null, + ), + ) + .toList(), ), ), ), @@ -422,7 +436,14 @@ class _ActivitiesPageState extends State Widget _buildPriceFilterSheet() { final theme = Theme.of(context); - final prices = ['Prix', 'Gratuit', 'Bon marché', 'Modéré', 'Cher', 'Très cher']; + final prices = [ + 'Prix', + 'Gratuit', + 'Bon marché', + 'Modéré', + 'Cher', + 'Très cher', + ]; return Container( padding: const EdgeInsets.all(16), @@ -443,19 +464,26 @@ class _ActivitiesPageState extends State Flexible( child: SingleChildScrollView( child: Column( - children: prices.map((price) => ListTile( - title: Text(price), - onTap: () { - setState(() { - _selectedPrice = price; - }); - Navigator.pop(context); - _applyFilters(); - }, - trailing: _selectedPrice == price - ? Icon(Icons.check, color: theme.colorScheme.primary) - : null, - )).toList(), + children: prices + .map( + (price) => ListTile( + title: Text(price), + onTap: () { + setState(() { + _selectedPrice = price; + }); + Navigator.pop(context); + _applyFilters(); + }, + trailing: _selectedPrice == price + ? Icon( + Icons.check, + color: theme.colorScheme.primary, + ) + : null, + ), + ) + .toList(), ), ), ), @@ -466,7 +494,13 @@ class _ActivitiesPageState extends State Widget _buildRatingFilterSheet() { final theme = Theme.of(context); - final ratings = ['Note', '4+ étoiles', '3+ étoiles', '2+ étoiles', '1+ étoiles']; + final ratings = [ + 'Note', + '4+ étoiles', + '3+ étoiles', + '2+ étoiles', + '1+ étoiles', + ]; return Container( padding: const EdgeInsets.all(16), @@ -487,19 +521,26 @@ class _ActivitiesPageState extends State Flexible( child: SingleChildScrollView( child: Column( - children: ratings.map((rating) => ListTile( - title: Text(rating), - onTap: () { - setState(() { - _selectedRating = rating; - }); - Navigator.pop(context); - _applyFilters(); - }, - trailing: _selectedRating == rating - ? Icon(Icons.check, color: theme.colorScheme.primary) - : null, - )).toList(), + children: ratings + .map( + (rating) => ListTile( + title: Text(rating), + onTap: () { + setState(() { + _selectedRating = rating; + }); + Navigator.pop(context); + _applyFilters(); + }, + trailing: _selectedRating == rating + ? Icon( + Icons.check, + color: theme.colorScheme.primary, + ) + : null, + ), + ) + .toList(), ), ), ), @@ -509,13 +550,14 @@ class _ActivitiesPageState extends State } void _applyFilters() { - String? category = _selectedCategory == 'Toutes les catégories' ? null : _selectedCategory; + String? category = _selectedCategory == 'Toutes les catégories' + ? null + : _selectedCategory; double? minRating = _getMinRatingFromString(_selectedRating); - - context.read().add(FilterActivities( - category: category, - minRating: minRating, - )); + + context.read().add( + FilterActivities(category: category, minRating: minRating), + ); } double? _getMinRatingFromString(String rating) { @@ -602,15 +644,20 @@ class _ActivitiesPageState extends State if (state is ActivitySearchResults) { final googleActivities = state.searchResults; - + // Filtrer les activités déjà présentes dans le voyage final filteredActivities = googleActivities.where((googleActivity) { - return !_tripActivities.any((tripActivity) => - tripActivity.name.toLowerCase().trim() == googleActivity.name.toLowerCase().trim()); + return !_tripActivities.any( + (tripActivity) => + tripActivity.name.toLowerCase().trim() == + googleActivity.name.toLowerCase().trim(), + ); }).toList(); - - print('🔍 [Google Search] ${googleActivities.length} résultats trouvés, ${filteredActivities.length} après filtrage'); - + + print( + '🔍 [Google Search] ${googleActivities.length} résultats trouvés, ${filteredActivities.length} après filtrage', + ); + if (filteredActivities.isEmpty && googleActivities.isNotEmpty) { return Column( mainAxisAlignment: MainAxisAlignment.center, @@ -626,13 +673,16 @@ class _ActivitiesPageState extends State icon: const Icon(Icons.add_circle_outline), label: const Text('Rechercher plus d\'activités'), style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), ), ), ], ); } - + if (filteredActivities.isEmpty) { return Column( mainAxisAlignment: MainAxisAlignment.center, @@ -648,7 +698,10 @@ class _ActivitiesPageState extends State icon: const Icon(Icons.refresh), label: const Text('Rechercher à nouveau'), style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), ), ), ], @@ -668,19 +721,20 @@ class _ActivitiesPageState extends State itemCount: filteredActivities.length, itemBuilder: (context, index) { final activity = filteredActivities[index]; - return _buildActivityCard(activity, isGoogleSuggestion: true); + return _buildActivityCard( + activity, + isGoogleSuggestion: true, + ); }, ), ), ), - + // Bouton "Rechercher plus d'activités" if (state.isLoading) Container( padding: const EdgeInsets.all(16), - child: const Center( - child: CircularProgressIndicator(), - ), + child: const Center(child: CircularProgressIndicator()), ) else Column( @@ -694,7 +748,9 @@ class _ActivitiesPageState extends State label: const Text('Rechercher plus d\'activités'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), - backgroundColor: Theme.of(context).colorScheme.primary, + backgroundColor: Theme.of( + context, + ).colorScheme.primary, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), @@ -708,10 +764,18 @@ class _ActivitiesPageState extends State padding: const EdgeInsets.symmetric(horizontal: 16), child: OutlinedButton.icon( onPressed: () { - print('🧪 [DEBUG] Force auto-reload check - État actuel:'); - print('🧪 [DEBUG] _tripActivities: ${_tripActivities.length}'); - print('🧪 [DEBUG] _autoReloadInProgress: $_autoReloadInProgress'); - print('🧪 [DEBUG] _lastAutoReloadTriggerCount: $_lastAutoReloadTriggerCount'); + print( + '🧪 [DEBUG] Force auto-reload check - État actuel:', + ); + print( + '🧪 [DEBUG] _tripActivities: ${_tripActivities.length}', + ); + print( + '🧪 [DEBUG] _autoReloadInProgress: $_autoReloadInProgress', + ); + print( + '🧪 [DEBUG] _lastAutoReloadTriggerCount: $_lastAutoReloadTriggerCount', + ); _checkAndLoadMoreActivitiesIfNeeded(); }, icon: const Icon(Icons.bug_report, size: 16), @@ -742,7 +806,10 @@ class _ActivitiesPageState extends State icon: const Icon(Icons.search), label: const Text('Rechercher des activités (6 résultats)'), style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Colors.white, ), @@ -768,11 +835,7 @@ class _ActivitiesPageState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - icon, - size: 64, - color: theme.colorScheme.outline, - ), + Icon(icon, size: 64, color: theme.colorScheme.outline), const SizedBox(height: 16), Text( title, @@ -808,38 +871,45 @@ class _ActivitiesPageState extends State _loadActivities(); } }, - child: activities.isEmpty - ? ListView( - children: [ - SizedBox( - height: MediaQuery.of(context).size.height * 0.6, - child: _buildEmptyState( - 'Aucune activité', - 'Tirez vers le bas pour actualiser', - Icons.refresh, + child: activities.isEmpty + ? ListView( + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.6, + child: _buildEmptyState( + 'Aucune activité', + 'Tirez vers le bas pour actualiser', + Icons.refresh, + ), ), - ), - ], - ) - : ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: activities.length, - itemBuilder: (context, index) { - final activity = activities[index]; - return _buildActivityCard(activity); - }, - ), + ], + ) + : ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: activities.length, + itemBuilder: (context, index) { + final activity = activities[index]; + return _buildActivityCard(activity); + }, + ), ); } - Widget _buildActivityCard(Activity activity, {bool isGoogleSuggestion = false}) { + Widget _buildActivityCard( + Activity activity, { + bool isGoogleSuggestion = false, + }) { final theme = Theme.of(context); - + // Vérifier si l'activité existe déjà dans le voyage (pour les suggestions Google) - final bool activityAlreadyExists = isGoogleSuggestion && - _tripActivities.any((tripActivity) => - tripActivity.name.toLowerCase().trim() == activity.name.toLowerCase().trim()); - + final bool activityAlreadyExists = + isGoogleSuggestion && + _tripActivities.any( + (tripActivity) => + tripActivity.name.toLowerCase().trim() == + activity.name.toLowerCase().trim(), + ); + return Card( margin: const EdgeInsets.only(bottom: 12), child: Column( @@ -848,8 +918,10 @@ class _ActivitiesPageState extends State // Image de l'activité if (activity.imageUrl != null && activity.imageUrl!.isNotEmpty) ClipRRect( - borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), - child: Container( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12), + ), + child: SizedBox( height: 200, width: double.infinity, child: Image.network( @@ -887,7 +959,7 @@ class _ActivitiesPageState extends State child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! + loadingProgress.expectedTotalBytes! : null, ), ), @@ -896,7 +968,7 @@ class _ActivitiesPageState extends State ), ), ), - + // Contenu de la carte Padding( padding: const EdgeInsets.all(16), @@ -942,7 +1014,10 @@ class _ActivitiesPageState extends State // Note if (activity.rating != null) Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), decoration: BoxDecoration( color: Colors.amber.withOpacity(0.1), borderRadius: BorderRadius.circular(12), @@ -950,7 +1025,11 @@ class _ActivitiesPageState extends State child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.star, color: Colors.amber, size: 16), + const Icon( + Icons.star, + color: Colors.amber, + size: 16, + ), const SizedBox(width: 4), Text( activity.rating!.toStringAsFixed(1), @@ -1004,11 +1083,16 @@ class _ActivitiesPageState extends State if (activityAlreadyExists) ...[ // Activité déjà dans le voyage Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), decoration: BoxDecoration( color: Colors.orange.withOpacity(0.1), borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.orange.withOpacity(0.3)), + border: Border.all( + color: Colors.orange.withOpacity(0.3), + ), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -1055,7 +1139,10 @@ class _ActivitiesPageState extends State children: [ // Votes positifs (pouces verts) Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), decoration: BoxDecoration( color: Colors.green.withOpacity(0.1), borderRadius: BorderRadius.circular(12), @@ -1082,7 +1169,10 @@ class _ActivitiesPageState extends State const SizedBox(width: 8), // Votes négatifs (pouces rouges) Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), decoration: BoxDecoration( color: Colors.red.withOpacity(0.1), borderRadius: BorderRadius.circular(12), @@ -1162,19 +1252,19 @@ class _ActivitiesPageState extends State void _voteForActivity(String activityId, int vote) { print('🗳️ Vote pour activité $activityId: $vote'); - + // TODO: Récupérer l'ID utilisateur actuel // Pour l'instant, on utilise un ID temporaire - final userId = 'current_user_id'; - - context.read().add(VoteForActivity( - activityId: activityId, - userId: userId, - vote: vote, - )); + final userId = 'current_user_id'; + + context.read().add( + VoteForActivity(activityId: activityId, userId: userId, vote: vote), + ); // Afficher un feedback à l'utilisateur - final message = vote == 1 ? 'Vote positif ajouté !' : 'Vote négatif ajouté !'; + final message = vote == 1 + ? 'Vote positif ajouté !' + : 'Vote négatif ajouté !'; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), @@ -1186,16 +1276,16 @@ class _ActivitiesPageState extends State void _addGoogleActivityToTrip(Activity activity) { print('➕ [Add Activity] Adding ${activity.name} to trip'); - + // Créer une nouvelle activité avec l'ID du voyage final newActivity = activity.copyWith( tripId: widget.trip.id, // Générer un nouvel ID unique pour cette activité dans le voyage id: DateTime.now().millisecondsSinceEpoch.toString(), ); - + context.read().add(AddActivity(newActivity)); - + // Afficher un feedback à l'utilisateur ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -1224,56 +1314,81 @@ class _ActivitiesPageState extends State final currentState = context.read().state; if (currentState is ActivitySearchResults) { final googleActivities = currentState.searchResults; - - print('🔍 [Auto-reload] Activités du voyage en mémoire: ${_tripActivities.length}'); - print('🔍 [Auto-reload] Activités Google total: ${googleActivities.length}'); - + + print( + '🔍 [Auto-reload] Activités du voyage en mémoire: ${_tripActivities.length}', + ); + print( + '🔍 [Auto-reload] Activités Google total: ${googleActivities.length}', + ); + // Filtrer les activités déjà présentes dans le voyage final filteredActivities = googleActivities.where((googleActivity) { - final isDuplicate = _tripActivities.any((tripActivity) => - tripActivity.name.toLowerCase().trim() == googleActivity.name.toLowerCase().trim()); + final isDuplicate = _tripActivities.any( + (tripActivity) => + tripActivity.name.toLowerCase().trim() == + googleActivity.name.toLowerCase().trim(), + ); if (isDuplicate) { print('🔍 [Auto-reload] Activité filtrée: ${googleActivity.name}'); } return !isDuplicate; }).toList(); - - print('🔍 [Auto-reload] ${filteredActivities.length} activités visibles après filtrage sur ${googleActivities.length} total'); - + + print( + '🔍 [Auto-reload] ${filteredActivities.length} activités visibles après filtrage sur ${googleActivities.length} total', + ); + // Protection: ne pas redéclencher pour le même nombre d'activités Google if (googleActivities.length == _lastAutoReloadTriggerCount) { - print('🔒 [Auto-reload] Même nombre qu\'avant (${googleActivities.length}), skip pour éviter la boucle'); + print( + '🔒 [Auto-reload] Même nombre qu\'avant (${googleActivities.length}), skip pour éviter la boucle', + ); return; } - + // Si on a moins de 4 activités visibles ET qu'on n'a pas déjà beaucoup d'activités Google if (filteredActivities.length < 4 && googleActivities.length < 20) { _autoReloadInProgress = true; _lastAutoReloadTriggerCount = googleActivities.length; - + // Calculer combien d'activités on doit demander - final activitiesNeeded = 6 - filteredActivities.length; // Manque pour arriver à 6 - final newTotalToRequest = googleActivities.length + activitiesNeeded + 6; // Activités actuelles + ce qui manque + buffer de 6 - - print('🔄 [Auto-reload] DÉCLENCHEMENT: Besoin de ${activitiesNeeded} activités supplémentaires'); - print('📊 [Auto-reload] Demande totale: ${newTotalToRequest} activités (actuellement: ${googleActivities.length})'); - + final activitiesNeeded = + 6 - filteredActivities.length; // Manque pour arriver à 6 + final newTotalToRequest = + googleActivities.length + + activitiesNeeded + + 6; // Activités actuelles + ce qui manque + buffer de 6 + + print( + '🔄 [Auto-reload] DÉCLENCHEMENT: Besoin de $activitiesNeeded activités supplémentaires', + ); + print( + '📊 [Auto-reload] Demande totale: $newTotalToRequest activités (actuellement: ${googleActivities.length})', + ); + // Mettre à jour le compteur et recharger avec le nouveau total _totalGoogleActivitiesRequested = newTotalToRequest; _loadMoreGoogleActivitiesWithTotal(newTotalToRequest); - + // Libérer le verrou après un délai Future.delayed(const Duration(seconds: 3), () { _autoReloadInProgress = false; print('🔓 [Auto-reload] Verrou libéré'); }); } else if (filteredActivities.length >= 4) { - print('✅ [Auto-reload] Suffisamment d\'activités visibles (${filteredActivities.length} >= 4)'); + print( + '✅ [Auto-reload] Suffisamment d\'activités visibles (${filteredActivities.length} >= 4)', + ); } else { - print('🚫 [Auto-reload] Trop d\'activités Google déjà chargées (${googleActivities.length} >= 20), arrêt auto-reload'); + print( + '🚫 [Auto-reload] Trop d\'activités Google déjà chargées (${googleActivities.length} >= 20), arrêt auto-reload', + ); } } else { - print('⚠️ [Auto-reload] État pas prêt pour auto-chargement: ${currentState.runtimeType}'); + print( + '⚠️ [Auto-reload] État pas prêt pour auto-chargement: ${currentState.runtimeType}', + ); } } @@ -1282,27 +1397,35 @@ class _ActivitiesPageState extends State _totalGoogleActivitiesRequested = 6; // Reset du compteur _autoReloadInProgress = false; // Reset des protections _lastAutoReloadTriggerCount = 0; - + // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles if (widget.trip.hasCoordinates) { - print('🌍 [Google Search] Using pre-geocoded coordinates: ${widget.trip.latitude}, ${widget.trip.longitude}'); - context.read().add(SearchActivitiesWithCoordinates( - tripId: widget.trip.id!, - latitude: widget.trip.latitude!, - longitude: widget.trip.longitude!, - category: null, // Rechercher dans toutes les catégories - maxResults: 6, // Charger 6 résultats à la fois - reset: true, // Nouveau flag pour reset - )); + print( + '🌍 [Google Search] Using pre-geocoded coordinates: ${widget.trip.latitude}, ${widget.trip.longitude}', + ); + context.read().add( + SearchActivitiesWithCoordinates( + tripId: widget.trip.id!, + latitude: widget.trip.latitude!, + longitude: widget.trip.longitude!, + category: null, // Rechercher dans toutes les catégories + maxResults: 6, // Charger 6 résultats à la fois + reset: true, // Nouveau flag pour reset + ), + ); } else { - print('⚠️ [Google Search] No coordinates available, falling back to destination geocoding'); - context.read().add(SearchActivities( - tripId: widget.trip.id!, - destination: widget.trip.location, - category: null, // Rechercher dans toutes les catégories - maxResults: 6, // Charger 6 résultats à la fois - reset: true, // Nouveau flag pour reset - )); + print( + '⚠️ [Google Search] No coordinates available, falling back to destination geocoding', + ); + context.read().add( + SearchActivities( + tripId: widget.trip.id!, + destination: widget.trip.location, + category: null, // Rechercher dans toutes les catégories + maxResults: 6, // Charger 6 résultats à la fois + reset: true, // Nouveau flag pour reset + ), + ); } _googleSearchPerformed = true; } @@ -1312,27 +1435,35 @@ class _ActivitiesPageState extends State _totalGoogleActivitiesRequested = 6; // Reset du compteur _autoReloadInProgress = false; // Reset des protections _lastAutoReloadTriggerCount = 0; - + // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles if (widget.trip.hasCoordinates) { - print('🌍 [Google Search] Using pre-geocoded coordinates: ${widget.trip.latitude}, ${widget.trip.longitude}'); - context.read().add(SearchActivitiesWithCoordinates( - tripId: widget.trip.id!, - latitude: widget.trip.latitude!, - longitude: widget.trip.longitude!, - category: null, - maxResults: 6, - reset: true, - )); + print( + '🌍 [Google Search] Using pre-geocoded coordinates: ${widget.trip.latitude}, ${widget.trip.longitude}', + ); + context.read().add( + SearchActivitiesWithCoordinates( + tripId: widget.trip.id!, + latitude: widget.trip.latitude!, + longitude: widget.trip.longitude!, + category: null, + maxResults: 6, + reset: true, + ), + ); } else { - print('⚠️ [Google Search] No coordinates available, falling back to destination geocoding'); - context.read().add(SearchActivities( - tripId: widget.trip.id!, - destination: widget.trip.location, - category: null, - maxResults: 6, - reset: true, - )); + print( + '⚠️ [Google Search] No coordinates available, falling back to destination geocoding', + ); + context.read().add( + SearchActivities( + tripId: widget.trip.id!, + destination: widget.trip.location, + category: null, + maxResults: 6, + reset: true, + ), + ); } _googleSearchPerformed = true; } @@ -1340,72 +1471,94 @@ class _ActivitiesPageState extends State void _loadMoreGoogleActivities() { print('📄 [Google Search] Loading more activities (next 6 results)'); final currentState = context.read().state; - + if (currentState is ActivitySearchResults) { final currentCount = currentState.searchResults.length; final newTotal = currentCount + 6; - print('📊 [Google Search] Current results count: $currentCount, requesting total: $newTotal'); - + print( + '📊 [Google Search] Current results count: $currentCount, requesting total: $newTotal', + ); + _totalGoogleActivitiesRequested = newTotal; - + // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles if (widget.trip.hasCoordinates) { - print('🌍 [Google Search] Using pre-geocoded coordinates for more results'); - context.read().add(SearchActivitiesWithCoordinates( - tripId: widget.trip.id!, - latitude: widget.trip.latitude!, - longitude: widget.trip.longitude!, - category: null, - maxResults: newTotal, // Demander le total cumulé - reset: true, // Reset pour avoir tous les résultats d'un coup - )); + print( + '🌍 [Google Search] Using pre-geocoded coordinates for more results', + ); + context.read().add( + SearchActivitiesWithCoordinates( + tripId: widget.trip.id!, + latitude: widget.trip.latitude!, + longitude: widget.trip.longitude!, + category: null, + maxResults: newTotal, // Demander le total cumulé + reset: true, // Reset pour avoir tous les résultats d'un coup + ), + ); } else { - print('⚠️ [Google Search] No coordinates available, falling back to destination geocoding'); - context.read().add(SearchActivities( - tripId: widget.trip.id!, - destination: widget.trip.location, - category: null, - maxResults: newTotal, // Demander le total cumulé - reset: true, // Reset pour avoir tous les résultats d'un coup - )); + print( + '⚠️ [Google Search] No coordinates available, falling back to destination geocoding', + ); + context.read().add( + SearchActivities( + tripId: widget.trip.id!, + destination: widget.trip.location, + category: null, + maxResults: newTotal, // Demander le total cumulé + reset: true, // Reset pour avoir tous les résultats d'un coup + ), + ); } } } void _loadMoreGoogleActivitiesWithTotal(int totalToRequest) { - print('📈 [Google Search] Loading activities with specific total: $totalToRequest'); - + print( + '📈 [Google Search] Loading activities with specific total: $totalToRequest', + ); + // Au lieu de reset, on utilise l'offset et append pour forcer plus de résultats final currentState = context.read().state; if (currentState is ActivitySearchResults) { final currentCount = currentState.searchResults.length; final additionalNeeded = totalToRequest - currentCount; - - print('📊 [Google Search] Current: $currentCount, Total demandé: $totalToRequest, Additional: $additionalNeeded'); - + + print( + '📊 [Google Search] Current: $currentCount, Total demandé: $totalToRequest, Additional: $additionalNeeded', + ); + if (additionalNeeded > 0) { // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles if (widget.trip.hasCoordinates) { - print('🌍 [Google Search] Using pre-geocoded coordinates for additional results'); - context.read().add(SearchActivitiesWithCoordinates( - tripId: widget.trip.id!, - latitude: widget.trip.latitude!, - longitude: widget.trip.longitude!, - category: null, - maxResults: additionalNeeded, - offset: currentCount, - appendToExisting: true, // Ajouter aux résultats existants - )); + print( + '🌍 [Google Search] Using pre-geocoded coordinates for additional results', + ); + context.read().add( + SearchActivitiesWithCoordinates( + tripId: widget.trip.id!, + latitude: widget.trip.latitude!, + longitude: widget.trip.longitude!, + category: null, + maxResults: additionalNeeded, + offset: currentCount, + appendToExisting: true, // Ajouter aux résultats existants + ), + ); } else { - print('⚠️ [Google Search] No coordinates available, falling back to destination geocoding'); - context.read().add(SearchActivities( - tripId: widget.trip.id!, - destination: widget.trip.location, - category: null, - maxResults: additionalNeeded, - offset: currentCount, - appendToExisting: true, // Ajouter aux résultats existants - )); + print( + '⚠️ [Google Search] No coordinates available, falling back to destination geocoding', + ); + context.read().add( + SearchActivities( + tripId: widget.trip.id!, + destination: widget.trip.location, + category: null, + maxResults: additionalNeeded, + offset: currentCount, + appendToExisting: true, // Ajouter aux résultats existants + ), + ); } } else { print('⚠️ [Google Search] Pas besoin de charger plus (déjà suffisant)'); @@ -1413,24 +1566,32 @@ class _ActivitiesPageState extends State } else { // Si pas de résultats existants, faire une recherche complète if (widget.trip.hasCoordinates) { - print('🌍 [Google Search] Using pre-geocoded coordinates for fresh search'); - context.read().add(SearchActivitiesWithCoordinates( - tripId: widget.trip.id!, - latitude: widget.trip.latitude!, - longitude: widget.trip.longitude!, - category: null, - maxResults: totalToRequest, - reset: true, - )); + print( + '🌍 [Google Search] Using pre-geocoded coordinates for fresh search', + ); + context.read().add( + SearchActivitiesWithCoordinates( + tripId: widget.trip.id!, + latitude: widget.trip.latitude!, + longitude: widget.trip.longitude!, + category: null, + maxResults: totalToRequest, + reset: true, + ), + ); } else { - print('⚠️ [Google Search] No coordinates available, falling back to destination geocoding'); - context.read().add(SearchActivities( - tripId: widget.trip.id!, - destination: widget.trip.location, - category: null, - maxResults: totalToRequest, - reset: true, - )); + print( + '⚠️ [Google Search] No coordinates available, falling back to destination geocoding', + ); + context.read().add( + SearchActivities( + tripId: widget.trip.id!, + destination: widget.trip.location, + category: null, + maxResults: totalToRequest, + reset: true, + ), + ); } } } diff --git a/lib/components/activities/add_activity_bottom_sheet.dart b/lib/components/activities/add_activity_bottom_sheet.dart index 9afeef9..e211a7b 100644 --- a/lib/components/activities/add_activity_bottom_sheet.dart +++ b/lib/components/activities/add_activity_bottom_sheet.dart @@ -11,10 +11,7 @@ import '../../services/error_service.dart'; class AddActivityBottomSheet extends StatefulWidget { final Trip trip; - const AddActivityBottomSheet({ - Key? key, - required this.trip, - }) : super(key: key); + const AddActivityBottomSheet({super.key, required this.trip}); @override State createState() => _AddActivityBottomSheetState(); @@ -26,7 +23,7 @@ class _AddActivityBottomSheetState extends State { final _descriptionController = TextEditingController(); final _addressController = TextEditingController(); final ErrorService _errorService = ErrorService(); - + ActivityCategory _selectedCategory = ActivityCategory.attraction; bool _isLoading = false; @@ -43,16 +40,11 @@ class _AddActivityBottomSheetState extends State { final theme = Theme.of(context); final mediaQuery = MediaQuery.of(context); final keyboardHeight = mediaQuery.viewInsets.bottom; - + return AnimatedContainer( duration: const Duration(milliseconds: 200), height: mediaQuery.size.height * 0.85, - margin: EdgeInsets.only( - left: 16, - right: 16, - top: 16, - bottom: 16, - ), + margin: EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 16), decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(20), @@ -167,7 +159,9 @@ class _AddActivityBottomSheetState extends State { onPressed: () => Navigator.pop(context), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), - side: BorderSide(color: theme.colorScheme.outline), + side: BorderSide( + color: theme.colorScheme.outline, + ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), @@ -193,7 +187,9 @@ class _AddActivityBottomSheetState extends State { height: 20, child: CircularProgressIndicator( strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), + valueColor: AlwaysStoppedAnimation( + Colors.white, + ), ), ) : const Text('Ajouter'), @@ -214,9 +210,9 @@ class _AddActivityBottomSheetState extends State { Widget _buildSectionTitle(String title) { return Text( title, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), ); } @@ -240,17 +236,17 @@ class _AddActivityBottomSheetState extends State { border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( - color: isDarkMode - ? Colors.white.withOpacity(0.2) - : Colors.black.withOpacity(0.2), + color: isDarkMode + ? Colors.white.withOpacity(0.2) + : Colors.black.withOpacity(0.2), ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( - color: isDarkMode - ? Colors.white.withOpacity(0.2) - : Colors.black.withOpacity(0.2), + color: isDarkMode + ? Colors.white.withOpacity(0.2) + : Colors.black.withOpacity(0.2), ), ), focusedBorder: OutlineInputBorder( @@ -265,15 +261,13 @@ class _AddActivityBottomSheetState extends State { Widget _buildCategorySelector() { final theme = Theme.of(context); - + return Container( padding: const EdgeInsets.all(16), 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.withOpacity(0.5)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -290,7 +284,7 @@ class _AddActivityBottomSheetState extends State { runSpacing: 8, children: ActivityCategory.values.map((category) { final isSelected = _selectedCategory == category; - + return GestureDetector( onTap: () { setState(() { @@ -298,12 +292,17 @@ class _AddActivityBottomSheetState extends State { }); }, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), decoration: BoxDecoration( color: isSelected ? Colors.blue : Colors.transparent, borderRadius: BorderRadius.circular(20), border: Border.all( - color: isSelected ? Colors.blue : theme.colorScheme.outline, + color: isSelected + ? Colors.blue + : theme.colorScheme.outline, ), ), child: Row( @@ -312,17 +311,17 @@ class _AddActivityBottomSheetState extends State { Icon( _getCategoryIcon(category), size: 16, - color: isSelected - ? Colors.white - : theme.colorScheme.onSurface, + color: isSelected + ? Colors.white + : theme.colorScheme.onSurface, ), const SizedBox(width: 6), Text( category.displayName, style: theme.textTheme.bodySmall?.copyWith( - color: isSelected - ? Colors.white - : theme.colorScheme.onSurface, + color: isSelected + ? Colors.white + : theme.colorScheme.onSurface, fontWeight: FontWeight.w500, ), ), @@ -361,19 +360,18 @@ class _AddActivityBottomSheetState extends State { name: _nameController.text.trim(), description: _descriptionController.text.trim(), category: _selectedCategory.displayName, - address: _addressController.text.trim().isNotEmpty - ? _addressController.text.trim() - : null, + address: _addressController.text.trim().isNotEmpty + ? _addressController.text.trim() + : null, votes: {}, createdAt: DateTime.now(), updatedAt: DateTime.now(), ); context.read().add(AddActivity(activity)); - + // Fermer le bottom sheet Navigator.pop(context); - } catch (e) { _errorService.showSnackbar( message: 'Erreur lors de l\'ajout de l\'activité', @@ -412,4 +410,4 @@ class _AddActivityBottomSheetState extends State { return Icons.spa; } } -} \ No newline at end of file +} diff --git a/lib/components/home/trip_card.dart b/lib/components/home/trip_card.dart index 7eb2c20..413d563 100644 --- a/lib/components/home/trip_card.dart +++ b/lib/components/home/trip_card.dart @@ -8,11 +8,7 @@ class TripCard extends StatefulWidget { final Trip trip; final VoidCallback? onTap; - const TripCard({ - super.key, - required this.trip, - this.onTap, - }); + const TripCard({super.key, required this.trip, this.onTap}); @override State createState() => _TripCardState(); @@ -29,7 +25,7 @@ class _TripCardState extends State { void initState() { super.initState(); _currentImageUrl = widget.trip.imageUrl; - + // Si aucune image n'est disponible, essayer de la charger if (_currentImageUrl == null || _currentImageUrl!.isEmpty) { _loadImageForTrip(); @@ -38,28 +34,29 @@ class _TripCardState extends State { Future _loadImageForTrip() async { if (_hasTriedLoading || _isLoadingImage) return; - + setState(() { _isLoadingImage = true; _hasTriedLoading = true; }); try { - // D'abord vérifier si une image existe déjà dans le Storage - String? imageUrl = await _placeImageService.getExistingImageUrl(widget.trip.location); - + String? imageUrl = await _placeImageService.getExistingImageUrl( + widget.trip.location, + ); + // Si aucune image n'existe, en télécharger une nouvelle - if (imageUrl == null) { - imageUrl = await _placeImageService.getPlaceImageUrl(widget.trip.location); - } - + imageUrl ??= await _placeImageService.getPlaceImageUrl( + widget.trip.location, + ); + if (mounted && imageUrl != null) { setState(() { _currentImageUrl = imageUrl; _isLoadingImage = false; }); - + // Mettre à jour le voyage dans la base de données avec l'imageUrl _updateTripWithImage(imageUrl); } else { @@ -84,7 +81,7 @@ class _TripCardState extends State { imageUrl: imageUrl, updatedAt: DateTime.now(), ); - + // Mettre à jour dans la base de données await _tripRepository.updateTrip(widget.trip.id!, updatedTrip); } @@ -95,7 +92,7 @@ class _TripCardState extends State { Widget _buildImageWidget() { final isDarkMode = Theme.of(context).brightness == Brightness.dark; - + if (_isLoadingImage) { return Container( color: isDarkMode ? Colors.grey[700] : Colors.grey[200], @@ -111,21 +108,20 @@ class _TripCardState extends State { ), ); } - + if (_currentImageUrl != null && _currentImageUrl!.isNotEmpty) { return CachedNetworkImage( imageUrl: _currentImageUrl!, fit: BoxFit.cover, placeholder: (context, url) => Container( color: Colors.grey[200], - child: const Center( - child: CircularProgressIndicator(), - ), + child: const Center(child: CircularProgressIndicator()), ), - errorWidget: (context, url, error) => _buildPlaceholderImage(isDarkMode), + errorWidget: (context, url, error) => + _buildPlaceholderImage(isDarkMode), ); } - + return _buildPlaceholderImage(isDarkMode); } @@ -140,9 +136,7 @@ class _TripCardState extends State { elevation: 4, margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), color: cardColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Material( color: Colors.transparent, child: InkWell( @@ -153,7 +147,9 @@ class _TripCardState extends State { children: [ // Image du voyage ClipRRect( - borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12), + ), child: SizedBox( height: 200, width: double.infinity, @@ -178,7 +174,7 @@ class _TripCardState extends State { overflow: TextOverflow.ellipsis, ), const SizedBox(height: 8), - + // Dates, participants et bouton voir Row( children: [ @@ -207,14 +203,17 @@ class _TripCardState extends State { ], ), ), - + // Bouton Voir ElevatedButton( onPressed: widget.onTap, style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 8, + ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), @@ -229,8 +228,8 @@ class _TripCardState extends State { ], ), ), - ], - ), + ], + ), ), ), ); @@ -261,4 +260,4 @@ class _TripCardState extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/models/activity.dart b/lib/models/activity.dart index 7f99e1d..d908e2c 100644 --- a/lib/models/activity.dart +++ b/lib/models/activity.dart @@ -70,7 +70,7 @@ class Activity { /// Vérifie si tous les participants du voyage ont voté positivement pour cette activité bool isApprovedByAllParticipants(List tripParticipants) { if (tripParticipants.isEmpty) return false; - + // Tous les participants doivent avoir voté for (String participantId in tripParticipants) { if (!votes.containsKey(participantId)) { @@ -80,7 +80,7 @@ class Activity { return false; // Quelqu'un a voté négativement ou neutre } } - + return true; // Tous ont voté positivement } @@ -183,7 +183,7 @@ class Activity { @override String toString() { - return 'Activity(id: $id, name: $name, category: $category, votes: ${totalVotes})'; + return 'Activity(id: $id, name: $name, category: $category, votes: $totalVotes)'; } @override @@ -245,4 +245,4 @@ enum PriceLevel { } return null; } -} \ No newline at end of file +} diff --git a/scripts/cleanup_images.dart b/scripts/cleanup_images.dart index 4d42b3a..cbd8179 100644 --- a/scripts/cleanup_images.dart +++ b/scripts/cleanup_images.dart @@ -1,32 +1,27 @@ import 'dart:io'; -import '../lib/services/trip_image_service.dart'; +import 'package:travel_mate/services/trip_image_service.dart'; /// Script utilitaire pour nettoyer les images inutilisées /// À exécuter manuellement si nécessaire void main() async { - try { final tripImageService = TripImageService(); - + // Remplacez par votre ID utilisateur // Vous pouvez le récupérer depuis Firebase Auth dans votre app const userId = 'YOUR_USER_ID_HERE'; - + if (userId == 'YOUR_USER_ID_HERE') { return; } - + final stats = await tripImageService.getImageStatistics(userId); - - + if (stats['tripsWithImages'] > 0) { await tripImageService.cleanupUnusedImages(userId); - + final newStats = await tripImageService.getImageStatistics(userId); - } else { - } - - + } else {} } catch (e) { exit(1); } @@ -42,4 +37,4 @@ Instructions d'utilisation: 4. Récupérez votre User ID affiché dans la console 5. Remplacez 'YOUR_USER_ID_HERE' par votre ID dans ce script 6. Exécutez: dart run scripts/cleanup_images.dart -*/ \ No newline at end of file +*/ diff --git a/scripts/diagnose_images.dart b/scripts/diagnose_images.dart index 0eb9976..5e9c73a 100644 --- a/scripts/diagnose_images.dart +++ b/scripts/diagnose_images.dart @@ -1,48 +1,47 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_storage/firebase_storage.dart'; -import '../lib/firebase_options.dart'; +import 'package:travel_mate/firebase_options.dart'; /// Script de diagnostic pour analyser les images dans Firebase Storage void main() async { - try { // Initialiser Firebase await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); - + final storage = FirebaseStorage.instance; - + final listResult = await storage.ref('trip_images').listAll(); - + if (listResult.items.isEmpty) { return; } - - + final Map>> locationGroups = {}; - + for (int i = 0; i < listResult.items.length; i++) { final item = listResult.items[i]; final fileName = item.name; - - + try { // Récupérer les métadonnées final metadata = await item.getMetadata(); final customMeta = metadata.customMetadata ?? {}; - + final location = customMeta['location'] ?? 'Inconnue'; - final normalizedLocation = customMeta['normalizedLocation'] ?? 'Non définie'; + final normalizedLocation = + customMeta['normalizedLocation'] ?? 'Non définie'; final source = customMeta['source'] ?? 'Inconnue'; final uploadedAt = customMeta['uploadedAt'] ?? 'Inconnue'; - - + // Récupérer l'URL de téléchargement final downloadUrl = await item.getDownloadURL(); - + // Grouper par location normalisée - final groupKey = normalizedLocation != 'Non définie' ? normalizedLocation : location.toLowerCase(); + final groupKey = normalizedLocation != 'Non définie' + ? normalizedLocation + : location.toLowerCase(); if (!locationGroups.containsKey(groupKey)) { locationGroups[groupKey] = []; } @@ -53,14 +52,12 @@ void main() async { 'uploadedAt': uploadedAt, 'downloadUrl': downloadUrl, }); - } catch (e) { - // Essayer de deviner la location depuis le nom du fichier final parts = fileName.split('_'); if (parts.length >= 2) { final guessedLocation = parts.take(parts.length - 1).join('_'); - + if (!locationGroups.containsKey(guessedLocation)) { locationGroups[guessedLocation] = []; } @@ -73,30 +70,24 @@ void main() async { }); } } - } - + // Analyser les doublons - + int totalDuplicates = 0; for (final entry in locationGroups.entries) { final location = entry.key; final images = entry.value; - + if (images.length > 1) { totalDuplicates += images.length - 1; - + for (int i = 0; i < images.length; i++) { final image = images[i]; } - } else { - } + } else {} } - - - if (totalDuplicates > 0) { - } - - } catch (e) { - } -} \ No newline at end of file + + if (totalDuplicates > 0) {} + } catch (e) {} +} diff --git a/test/image_loading_optimization_test.dart b/test/image_loading_optimization_test.dart index 6bda395..dfee098 100644 --- a/test/image_loading_optimization_test.dart +++ b/test/image_loading_optimization_test.dart @@ -4,43 +4,44 @@ void main() { group('Image Loading Logic Tests', () { test('should demonstrate the new flow without duplicate downloads', () { // Simulation du nouveau flux de chargement d'images - + // Scénario 1: Premier chargement (aucune image existante) print('=== Scénario 1: Premier chargement ==='); - String? existingImage = null; // Aucune image dans le Storage - + String? existingImage; // Aucune image dans le Storage + if (existingImage == null) { print('✓ Aucune image existante trouvée'); print('✓ Téléchargement d\'une nouvelle image depuis Google Places'); existingImage = 'https://storage.googleapis.com/image1.jpg'; print('✓ Image sauvée: $existingImage'); } - + expect(existingImage, isNotNull); - + // Scénario 2: Rechargement (image existante) print('\n=== Scénario 2: Rechargement avec image existante ==='); String? cachedImage = existingImage; // Image déjà dans le Storage - + print('✓ Image existante trouvée: $cachedImage'); print('✓ PAS de nouveau téléchargement'); print('✓ Réutilisation de l\'image existante'); - + expect(cachedImage, equals(existingImage)); - + // Scénario 3: Différente destination print('\n=== Scénario 3: Destination différente ==='); - String? differentLocationImage = null; // Pas d'image pour cette nouvelle destination - + String? + differentLocationImage; // Pas d'image pour cette nouvelle destination + if (differentLocationImage == null) { print('✓ Nouvelle destination, aucune image existante'); print('✓ Téléchargement autorisé pour cette nouvelle destination'); differentLocationImage = 'https://storage.googleapis.com/image2.jpg'; } - + expect(differentLocationImage, isNotNull); expect(differentLocationImage, isNot(equals(existingImage))); - + print('\n=== Résumé ==='); print('• Image pour destination 1: $existingImage'); print('• Image pour destination 2: $differentLocationImage'); @@ -52,9 +53,15 @@ void main() { final testCases = [ {'input': 'Paris, France', 'expected': 'paris_france'}, {'input': 'New York City', 'expected': 'new_york_city'}, - {'input': 'São Paulo', 'expected': 's_o_paulo'}, // Caractères spéciaux remplacés + { + 'input': 'São Paulo', + 'expected': 's_o_paulo', + }, // Caractères spéciaux remplacés {'input': 'Londres, Royaume-Uni', 'expected': 'londres_royaume_uni'}, - {'input': 'Tokyo (東京)', 'expected': 'tokyo'}, // Caractères non-latins supprimés + { + 'input': 'Tokyo (東京)', + 'expected': 'tokyo', + }, // Caractères non-latins supprimés ]; for (final testCase in testCases) { @@ -66,28 +73,28 @@ void main() { test('should demonstrate memory and performance benefits', () { // Simulation des bénéfices de performance - + final oldSystem = { - 'apiCalls': 4, // 4 appels à chaque chargement - 'storageWrites': 4, // 4 écritures dans le Storage - 'storageReads': 0, // Pas de vérification existante - 'dataUsage': '4.8 MB', // 4 images × 1.2 MB chacune + 'apiCalls': 4, // 4 appels à chaque chargement + 'storageWrites': 4, // 4 écritures dans le Storage + 'storageReads': 0, // Pas de vérification existante + 'dataUsage': '4.8 MB', // 4 images × 1.2 MB chacune }; final newSystem = { - 'apiCalls': 2, // Seulement pour les nouvelles destinations - 'storageWrites': 2, // Seulement pour les nouvelles images - 'storageReads': 2, // Vérifications d'existence - 'dataUsage': '2.4 MB', // Seulement 2 images nécessaires + 'apiCalls': 2, // Seulement pour les nouvelles destinations + 'storageWrites': 2, // Seulement pour les nouvelles images + 'storageReads': 2, // Vérifications d'existence + 'dataUsage': '2.4 MB', // Seulement 2 images nécessaires }; print('=== Comparaison de performance ==='); print('Ancien système:'); oldSystem.forEach((key, value) => print(' $key: $value')); - + print('\nNouveau système:'); newSystem.forEach((key, value) => print(' $key: $value')); - + print('\nAméliorations:'); print(' • API calls: -50%'); print(' • Storage writes: -50%'); @@ -113,4 +120,4 @@ String _normalizeLocationName(String location) { .replaceAll(RegExp(r'[^a-z0-9]'), '_') .replaceAll(RegExp(r'_+'), '_') .replaceAll(RegExp(r'^_|_$'), ''); -} \ No newline at end of file +} diff --git a/test/photo_quality_test.dart b/test/photo_quality_test.dart index 96ac5ee..84faa4f 100644 --- a/test/photo_quality_test.dart +++ b/test/photo_quality_test.dart @@ -11,42 +11,47 @@ void main() { {'width': 1200, 'height': 800, 'photo_reference': 'horizontal_hd'}, {'width': 600, 'height': 400, 'photo_reference': 'horizontal2'}, ]; - + // Appliquer l'algorithme de tri de qualité photos.sort((a, b) => _sortPhotosByQuality(a, b)); - + // Vérifications expect(photos.first['photo_reference'], 'horizontal_hd'); expect(photos.first['width'], 1200); expect(photos.first['height'], 800); - + // La photo verticale devrait être parmi les dernières final lastPhotos = photos.sublist(photos.length - 2); expect(lastPhotos.any((p) => p['photo_reference'] == 'vertical1'), true); - + print('Photos triées par qualité (meilleure en premier):'); for (var photo in photos) { final width = photo['width'] as int; final height = photo['height'] as int; final ratio = width / height; final resolution = width * height; - print('${photo['photo_reference']}: ${width}x${height} ' - '(ratio: ${ratio.toStringAsFixed(2)}, résolution: $resolution)'); + print( + '${photo['photo_reference']}: ${width}x$height ' + '(ratio: ${ratio.toStringAsFixed(2)}, résolution: $resolution)', + ); } }); test('should generate correct search terms for Paris', () { final searchTerms = _generateSearchTerms('Paris'); - + // Vérifier que les termes spécifiques à Paris sont présents expect(searchTerms.any((term) => term.contains('Tour Eiffel')), true); expect(searchTerms.any((term) => term.contains('Arc de Triomphe')), true); expect(searchTerms.any((term) => term.contains('Notre-Dame')), true); - + // Vérifier que les termes génériques sont aussi présents - expect(searchTerms.any((term) => term.contains('attractions touristiques')), true); + expect( + searchTerms.any((term) => term.contains('attractions touristiques')), + true, + ); expect(searchTerms.any((term) => term.contains('landmarks')), true); - + print('Termes de recherche pour Paris:'); for (var term in searchTerms) { print('- $term'); @@ -55,12 +60,12 @@ void main() { test('should generate correct search terms for London', () { final searchTerms = _generateSearchTerms('London'); - + // Vérifier que les termes spécifiques à Londres sont présents expect(searchTerms.any((term) => term.contains('Big Ben')), true); expect(searchTerms.any((term) => term.contains('Tower Bridge')), true); expect(searchTerms.any((term) => term.contains('London Eye')), true); - + print('Termes de recherche pour Londres:'); for (var term in searchTerms) { print('- $term'); @@ -69,15 +74,18 @@ void main() { test('should handle unknown cities gracefully', () { final searchTerms = _generateSearchTerms('Petite Ville Inconnue'); - + // Devrait au moins avoir des termes génériques expect(searchTerms.isNotEmpty, true); - expect(searchTerms.any((term) => term.contains('attractions touristiques')), true); + expect( + searchTerms.any((term) => term.contains('attractions touristiques')), + true, + ); expect(searchTerms.any((term) => term.contains('landmarks')), true); - + // Le terme original devrait être en dernier expect(searchTerms.last, 'Petite Ville Inconnue'); - + print('Termes de recherche pour ville inconnue:'); for (var term in searchTerms) { print('- $term'); @@ -92,39 +100,39 @@ int _sortPhotosByQuality(Map a, Map b) { final aHeight = a['height'] as int; final bWidth = b['width'] as int; final bHeight = b['height'] as int; - + final aRatio = aWidth / aHeight; final bRatio = bWidth / bHeight; - + // 1. Privilégier les photos horizontales (ratio > 1) if (aRatio > 1 && bRatio <= 1) return -1; if (bRatio > 1 && aRatio <= 1) return 1; - + // 2. Si les deux sont horizontales ou les deux ne le sont pas, // privilégier la résolution plus élevée final aResolution = aWidth * aHeight; final bResolution = bWidth * bHeight; - + if (aResolution != bResolution) { return bResolution.compareTo(aResolution); } - + // 3. En cas d'égalité de résolution, privilégier le meilleur ratio (plus proche de 1.5) final idealRatio = 1.5; final aDiff = (aRatio - idealRatio).abs(); final bDiff = (bRatio - idealRatio).abs(); - + return aDiff.compareTo(bDiff); } /// Reproduit l'algorithme de génération de termes de recherche List _generateSearchTerms(String location) { final terms = []; - + // Ajouter des termes spécifiques pour les villes connues final citySpecificTerms = _getCitySpecificTerms(location.toLowerCase()); terms.addAll(citySpecificTerms); - + // Termes génériques avec attractions terms.addAll([ '$location attractions touristiques monuments', @@ -136,14 +144,14 @@ List _generateSearchTerms(String location) { '$location skyline', location, // Terme original en dernier ]); - + return terms; } /// Reproduit les termes spécifiques pour des villes connues List _getCitySpecificTerms(String location) { final specific = []; - + if (location.contains('paris')) { specific.addAll([ 'Tour Eiffel Paris', @@ -182,6 +190,6 @@ List _getCitySpecificTerms(String location) { 'Tokyo Skytree', ]); } - + return specific; -} \ No newline at end of file +}