import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:firebase_auth/firebase_auth.dart'; import '../../blocs/activity/activity_bloc.dart'; import '../../blocs/activity/activity_event.dart'; import '../../blocs/activity/activity_state.dart'; import '../../models/trip.dart'; import '../../models/activity.dart'; import '../../services/activity_cache_service.dart'; import '../loading/laoding_content.dart'; import '../../blocs/user/user_bloc.dart'; import '../../blocs/user/user_state.dart'; import '../../services/error_service.dart'; import 'activity_detail_dialog.dart'; class ActivitiesPage extends StatefulWidget { final Trip trip; const ActivitiesPage({super.key, required this.trip}); @override State createState() => _ActivitiesPageState(); } class _ActivitiesPageState extends State with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { late TabController _tabController; final TextEditingController _searchController = TextEditingController(); final ActivityCacheService _cacheService = ActivityCacheService(); // 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; 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 @override 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) { _handleTabChange(); } }); } @override void didChangeDependencies() { super.didChangeDependencies(); // Recharger si nécessaire quand on revient sur la page if (!_activitiesLoaded) { _loadActivitiesIfNeeded(); } } void _loadActivitiesIfNeeded() { final state = context.read().state; if (state is! ActivityLoaded || !_activitiesLoaded) { _loadActivities(); } } void _handleTabChange() { // Si on va sur l'onglet suggestions Google et qu'aucune recherche n'a été faite if (_tabController.index == 2 && !_googleSearchPerformed) { // Vérifier si on a des activités en cache final cachedActivities = _cacheService.getCachedActivities( widget.trip.id!, ); if (cachedActivities != null && cachedActivities.isNotEmpty) { // Restaurer les activités en cache dans le BLoC context.read().add( RestoreCachedSearchResults(searchResults: cachedActivities), ); _googleSearchPerformed = true; } else { // Sinon, faire une nouvelle recherche _searchGoogleActivities(); } } } @override void dispose() { _tabController.dispose(); _searchController.dispose(); super.dispose(); } void _loadActivities() { setState(() { _isLoadingTripActivities = true; }); context.read().add(LoadActivities(widget.trip.id!)); _activitiesLoaded = true; } @override Widget build(BuildContext context) { super.build(context); // Nécessaire pour AutomaticKeepAliveClientMixin final theme = Theme.of(context); return BlocListener( listener: (context, state) { if (state is ActivityError) { ErrorService().showError( message: state.message, onRetry: () { if (_tabController.index == 2) { _searchGoogleActivities(); } else { _loadActivities(); } }, ); } if (state is ActivityAdded) { // Fermer le dialog de loading if (Navigator.of(context).canPop()) { Navigator.of(context).pop(); } // Ajouter l'activité à la liste locale des activités du voyage WidgetsBinding.instance.addPostFrameCallback((_) { setState(() { _tripActivities.add(state.activity); }); // Afficher un feedback de succès // Afficher un feedback de succès ErrorService().showSnackbar( message: '${state.activity.name} ajoutée au voyage !', isError: false, onRetry: () { // Revenir à l'onglet des activités du voyage _tabController.animateTo(0); }, ); }); } if (state is ActivityLoaded) { // Stocker les activités localement WidgetsBinding.instance.addPostFrameCallback((_) { setState(() { final allActivities = state.filteredActivities; _approvedActivities = allActivities .where( (a) => a.isApprovedByAllParticipants([ ...widget.trip.participants, widget.trip.createdBy, ]), ) .toList(); _tripActivities = allActivities .where( (a) => !a.isApprovedByAllParticipants([ ...widget.trip.participants, widget.trip.createdBy, ]), ) .toList(); _isLoadingTripActivities = false; }); // Vérifier si on a besoin de charger plus d'activités dans les suggestions Future.delayed(const Duration(milliseconds: 500), () { _checkAndLoadMoreActivitiesIfNeeded(); }); }); } if (state is ActivitySearchResults) { // Gérer l'ajout d'activité depuis les résultats de recherche if (state.newlyAddedActivity != null) { // Fermer le dialog de loading s'il est ouvert if (Navigator.of(context).canPop()) { Navigator.of(context).pop(); } WidgetsBinding.instance.addPostFrameCallback((_) { setState(() { _tripActivities.add(state.newlyAddedActivity!); }); ErrorService().showSnackbar( message: '${state.newlyAddedActivity!.name} ajoutée au voyage !', isError: false, onRetry: () { _tabController.animateTo(0); }, ); }); } // 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((_) { Future.delayed(const Duration(milliseconds: 500), () { _checkAndLoadMoreActivitiesIfNeeded(); }); }); } } if (state is ActivityVoteRecorded) { // Recharger les activités du voyage pour mettre à jour les votes _loadActivities(); } }, child: Scaffold( backgroundColor: theme.colorScheme.surface, appBar: AppBar( title: Text( 'Voyage à ${widget.trip.location}', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), backgroundColor: theme.colorScheme.surface, elevation: 0, foregroundColor: theme.colorScheme.onSurface, actions: [], ), body: Column( children: [ // Barre de recherche _buildSearchBar(theme), // Onglets de catégories _buildCategoryTabs(theme), // Contenu des onglets Expanded( child: TabBarView( controller: _tabController, children: [ _buildTripActivitiesTab(), _buildApprovedActivitiesTab(), _buildGoogleSuggestionsTab(), ], ), ), ], ), ), ); } Widget _buildSearchBar(ThemeData theme) { return Container( padding: const EdgeInsets.all(16), child: Container( decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest.withValues( alpha: 0.3, ), borderRadius: BorderRadius.circular(12), ), child: TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Rechercher restaurants, musées...', hintStyle: TextStyle( color: theme.colorScheme.onSurface.withValues(alpha: 0.6), ), prefixIcon: Icon( Icons.search, color: theme.colorScheme.onSurface.withValues(alpha: 0.6), ), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), ), onSubmitted: (value) { if (value.isNotEmpty) { _performSearch(value); } }, ), ), ); } Widget _buildCategoryTabs(ThemeData theme) { return Container( padding: const EdgeInsets.all(16), child: Container( decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest.withValues( alpha: 0.3, ), borderRadius: BorderRadius.circular(8), ), child: TabBar( controller: _tabController, labelColor: Colors.white, unselectedLabelColor: theme.colorScheme.onSurface.withValues( alpha: 0.7, ), indicator: BoxDecoration( color: theme.colorScheme.primary, borderRadius: BorderRadius.circular(6), ), indicatorSize: TabBarIndicatorSize.tab, dividerColor: Colors.transparent, tabs: const [ Tab(text: 'Voyage'), Tab(text: 'Approuvées'), Tab(text: 'Suggestion'), ], ), ), ); } Widget _buildTripActivitiesTab() { // Utiliser les données locales au lieu du BLoC if (_isLoadingTripActivities) { return const Center(child: CircularProgressIndicator()); } if (_tripActivities.isEmpty) { return _buildEmptyState( 'Aucune activité du voyage', 'Ajoutez vos premières activités pour ce voyage', Icons.add_location, ); } return _buildActivityList(_tripActivities); } Widget _buildApprovedActivitiesTab() { // Utiliser les données locales au lieu du BLoC if (_isLoadingTripActivities) { return const Center(child: CircularProgressIndicator()); } if (_approvedActivities.isEmpty) { return _buildEmptyState( 'Aucune activité approuvée', 'Les activités avec des votes positifs apparaîtront ici', Icons.thumb_up_outlined, ); } return _buildActivityList(_approvedActivities); } Widget _buildGoogleSuggestionsTab() { return BlocBuilder( builder: (context, state) { if (state is ActivitySearching) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator(), const SizedBox(height: 16), Text( 'Recherche d\'activités en cours...', style: Theme.of(context).textTheme.bodyMedium, ), ], ), ); } 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(), ); }).toList(); if (filteredActivities.isEmpty && googleActivities.isNotEmpty) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildEmptyState( 'Toutes les activités sont déjà dans votre voyage', 'Recherchez plus d\'activités pour découvrir de nouvelles suggestions', Icons.check_circle, ), const SizedBox(height: 20), ElevatedButton.icon( onPressed: () => _loadMoreGoogleActivities(), 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, ), ), ), ], ); } if (filteredActivities.isEmpty) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildEmptyState( 'Aucun résultat trouvé', 'Aucune activité trouvée pour cette destination', Icons.search_off, ), const SizedBox(height: 20), ElevatedButton.icon( onPressed: () => _searchGoogleActivities(), icon: const Icon(Icons.refresh), label: const Text('Rechercher à nouveau'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 12, ), ), ), ], ); } return Column( children: [ // Liste des activités Expanded( child: RefreshIndicator( onRefresh: () async { _resetAndSearchGoogleActivities(); }, child: ListView.builder( padding: const EdgeInsets.all(16), itemCount: filteredActivities.length, itemBuilder: (context, index) { final activity = filteredActivities[index]; 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()), ) else Column( children: [ Container( width: double.infinity, padding: const EdgeInsets.all(16), child: ElevatedButton.icon( onPressed: () => _loadMoreGoogleActivities(), icon: const Icon(Icons.add_circle_outline), label: const Text('Rechercher plus d\'activités'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), backgroundColor: Theme.of( context, ).colorScheme.primary, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ], ), ], ); } // État initial - aucune recherche effectuée return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildEmptyState( 'Découvrir des activités', 'Trouvez des restaurants, musées et attractions près de ${widget.trip.location}', Icons.explore, ), const SizedBox(height: 20), ElevatedButton.icon( onPressed: () => _searchGoogleActivities(), 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, ), backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Colors.white, ), ), const SizedBox(height: 16), Text( 'Recherche powered by Google Places', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of( context, ).colorScheme.onSurface.withValues(alpha: 0.6), ), ), ], ); }, ); } Widget _buildEmptyState(String title, String subtitle, IconData icon) { final theme = Theme.of(context); return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 64, color: theme.colorScheme.outline), const SizedBox(height: 16), Text( title, style: theme.textTheme.titleLarge?.copyWith( color: theme.colorScheme.onSurface, fontWeight: FontWeight.w600, ), textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( subtitle, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface.withValues(alpha: 0.7), ), textAlign: TextAlign.center, ), ], ), ), ); } Widget _buildActivityList(List activities) { return RefreshIndicator( onRefresh: () async { // Rafraîchir selon l'onglet actuel if (_tabController.index == 2) { // Onglet Google - relancer la recherche _searchGoogleActivities(); } else { // Onglets activités du voyage - recharger depuis la base de données _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, ), ), ], ) : 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, }) { 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(), ); return GestureDetector( onTap: () { showDialog( context: context, builder: (context) => ActivityDetailDialog(activity: activity), ); }, child: Card( margin: const EdgeInsets.only(bottom: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Image de l'activité if (activity.imageUrl != null && activity.imageUrl!.isNotEmpty) ClipRRect( borderRadius: const BorderRadius.vertical( top: Radius.circular(12), ), child: SizedBox( height: 200, width: double.infinity, child: Image.network( activity.imageUrl!, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( height: 200, color: theme.colorScheme.surfaceContainerHighest, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.image_not_supported, size: 48, color: theme.colorScheme.onSurfaceVariant, ), const SizedBox(height: 8), Text( 'Image non disponible', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ); }, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Container( height: 200, color: theme.colorScheme.surfaceContainerHighest, child: Center( child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, ), ), ); }, ), ), ), // Contenu de la carte Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ // Icône de catégorie Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: theme.colorScheme.primary.withValues( alpha: 0.1, ), borderRadius: BorderRadius.circular(8), ), child: Icon( _getCategoryIcon(activity.category), color: theme.colorScheme.primary, size: 20, ), ), const SizedBox(width: 12), // Nom et catégorie Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( activity.name, style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), Text( activity.category, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.primary, ), ), ], ), ), // Note if (activity.rating != null) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: Colors.amber.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.star, color: Colors.amber, size: 16, ), const SizedBox(width: 4), Text( activity.rating!.toStringAsFixed(1), style: theme.textTheme.bodySmall?.copyWith( fontWeight: FontWeight.w600, ), ), ], ), ), ], ), if (activity.description.isNotEmpty) ...[ const SizedBox(height: 12), Text( activity.description, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface.withValues( alpha: 0.8, ), ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], if (activity.address != null) ...[ const SizedBox(height: 8), Row( children: [ Icon( Icons.location_on, size: 16, color: theme.colorScheme.onSurface.withValues( alpha: 0.6, ), ), const SizedBox(width: 4), Expanded( child: Text( activity.address!, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurface.withValues( alpha: 0.6, ), ), ), ), ], ), ], const SizedBox(height: 12), // Boutons d'action et votes (différents selon le contexte) if (isGoogleSuggestion) ...[ // Pour les suggestions Google : bouton d'ajout ou indication si déjà ajoutée Row( children: [ if (activityAlreadyExists) ...[ // Activité déjà dans le voyage Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), decoration: BoxDecoration( color: Colors.orange.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), border: Border.all( color: Colors.orange.withValues(alpha: 0.3), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.check_circle, size: 16, color: Colors.orange.shade700, ), const SizedBox(width: 6), Text( 'Déjà dans le voyage', style: theme.textTheme.bodySmall?.copyWith( color: Colors.orange.shade700, fontWeight: FontWeight.w600, ), ), ], ), ), ] else ...[ // Bouton pour ajouter l'activité Expanded( child: ElevatedButton.icon( onPressed: () => _addGoogleActivityToTrip(activity), icon: const Icon(Icons.add, size: 18), label: const Text('Ajouter au voyage'), style: ElevatedButton.styleFrom( backgroundColor: theme.colorScheme.primary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( vertical: 12, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ], ], ), ] else ...[ // Pour les activités du voyage : système de votes Row( children: [ // Votes positifs (pouces verts) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: Colors.green.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( '${activity.positiveVotes}', style: theme.textTheme.bodySmall?.copyWith( color: Colors.green.shade700, fontWeight: FontWeight.w600, ), ), const SizedBox(width: 4), Icon( Icons.thumb_up, size: 16, color: Colors.green.shade700, ), ], ), ), const SizedBox(width: 8), // Votes négatifs (pouces rouges) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: Colors.red.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( '${activity.negativeVotes}', style: theme.textTheme.bodySmall?.copyWith( color: Colors.red.shade700, fontWeight: FontWeight.w600, ), ), const SizedBox(width: 4), Icon( Icons.thumb_down, size: 16, color: Colors.red.shade700, ), ], ), ), const Spacer(), // Bouton J'aime/J'aime pas IconButton( onPressed: () => _voteForActivity(activity.id, 1), icon: const Icon(Icons.thumb_up), iconSize: 20, ), IconButton( onPressed: () => _voteForActivity(activity.id, -1), icon: const Icon(Icons.thumb_down), iconSize: 20, ), ], ), ], ], ), ), ], ), ), ); } IconData _getCategoryIcon(String category) { switch (category.toLowerCase()) { case 'museum': case 'musée': return Icons.museum; case 'restaurant': return Icons.restaurant; case 'attraction': return Icons.place; case 'divertissement': case 'entertainment': return Icons.sports_esports; case 'shopping': return Icons.shopping_bag; case 'nature': return Icons.park; case 'culture': return Icons.account_balance; case 'vie nocturne': case 'nightlife': return Icons.nightlife; case 'sports': return Icons.sports; case 'détente': case 'relaxation': return Icons.spa; default: return Icons.place; } } void _voteForActivity(String activityId, int vote) { // Récupérer l'ID utilisateur actuel depuis le UserBloc final userState = context.read().state; final userId = userState is UserLoaded ? userState.user.id : widget.trip.createdBy; // Vérifier si l'activité existe dans la liste locale pour vérifier le vote // (car l'objet activity passé peut venir d'une liste filtrée ou autre) final currentActivity = _tripActivities.firstWhere( (a) => a.id == activityId, orElse: () => _approvedActivities.firstWhere( (a) => a.id == activityId, orElse: () => Activity( id: '', tripId: '', name: '', description: '', category: '', createdAt: DateTime.now(), updatedAt: DateTime.now(), ), ), ); // Si l'activité a été trouvée et que l'utilisateur a déjà voté if (currentActivity.id.isNotEmpty && currentActivity.hasUserVoted(userId)) { ErrorService().showSnackbar( message: 'Vous avez déjà voté pour cette activité', isError: true, ); return; } 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é !'; ErrorService().showSnackbar(message: message, isError: false); } void _addGoogleActivityToTrip(Activity activity) { // 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(), createdBy: FirebaseAuth.instance.currentUser?.uid, ); // Afficher le LoadingContent avec la tâche d'ajout showDialog( context: context, barrierDismissible: false, builder: (BuildContext dialogContext) { return LoadingContent( loadingText: 'Ajout de ${activity.name}...', onBackgroundTask: () async { // Ajouter l'activité au voyage context.read().add(AddActivity(newActivity)); // Attendre que l'ajout soit complété await Future.delayed(const Duration(milliseconds: 1000)); }, onComplete: () { // Fermer le dialog quand l'ajout est complété if (Navigator.of(context).canPop()) { Navigator.of(context).pop(); } }, ); }, ); } void _checkAndLoadMoreActivitiesIfNeeded() { // Protection contre les rechargements en boucle if (_autoReloadInProgress) { return; } final currentState = context.read().state; if (currentState is ActivitySearchResults) { final googleActivities = currentState.searchResults; // 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(), ); if (isDuplicate) {} return !isDuplicate; }).toList(); // Protection: ne pas redéclencher pour le même nombre d'activités Google if (googleActivities.length == _lastAutoReloadTriggerCount) { 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 // Mettre à jour le compteur et recharger avec le nouveau total _loadMoreGoogleActivitiesWithTotal(newTotalToRequest); // Libérer le verrou après un délai Future.delayed(const Duration(seconds: 3), () { _autoReloadInProgress = false; }); } else if (filteredActivities.length >= 4) { } else {} } else {} } void _searchGoogleActivities() { _autoReloadInProgress = false; // Reset des protections _lastAutoReloadTriggerCount = 0; // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles if (widget.trip.hasCoordinates) { 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 { 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; } void _resetAndSearchGoogleActivities() { _autoReloadInProgress = false; // Reset des protections _lastAutoReloadTriggerCount = 0; // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles if (widget.trip.hasCoordinates) { context.read().add( SearchActivitiesWithCoordinates( tripId: widget.trip.id!, latitude: widget.trip.latitude!, longitude: widget.trip.longitude!, category: null, maxResults: 6, reset: true, ), ); } else { context.read().add( SearchActivities( tripId: widget.trip.id!, destination: widget.trip.location, category: null, maxResults: 6, reset: true, ), ); } _googleSearchPerformed = true; } void _loadMoreGoogleActivities() { final currentState = context.read().state; if (currentState is ActivitySearchResults) { final currentCount = currentState.searchResults.length; final newTotal = currentCount + 6; // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles if (widget.trip.hasCoordinates) { 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 { 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) { // 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; if (additionalNeeded > 0) { // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles if (widget.trip.hasCoordinates) { 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 { 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 {} } else { // Si pas de résultats existants, faire une recherche complète if (widget.trip.hasCoordinates) { context.read().add( SearchActivitiesWithCoordinates( tripId: widget.trip.id!, latitude: widget.trip.latitude!, longitude: widget.trip.longitude!, category: null, maxResults: totalToRequest, reset: true, ), ); } else { context.read().add( SearchActivities( tripId: widget.trip.id!, destination: widget.trip.location, category: null, maxResults: totalToRequest, reset: true, ), ); } } } void _performSearch(String query) { // Basculer vers l'onglet suggestions _tabController.animateTo(2); // Déclencher la recherche textuelle context.read().add( SearchActivitiesByText( tripId: widget.trip.id!, destination: widget.trip.location, query: query, ), ); _googleSearchPerformed = true; } }